Project-substance authoring (L1-F)¶
The write surface for everything outside .colony/ — src/, tests/, data/, dossier/, models/, notebooks/, docs/. Symmetric companion to L1-E (meta-tooling write surface under .colony/). Both halves share the same audit pipeline (per Risk #5 in CPS_ALIGNMENT_PLAN.md) — same AST allow-list, same DesignCheckpointer-style commit attribution, same blackboard event protocol.
TL;DR¶
from polymathera.colony.design_monorepo import ProjectAuthoringCapability
cap = ProjectAuthoringCapability(agent=..., working_dir=repo_root)
await cap.write_file("src/opm_meg/serf/calibration.py", body)
await cap.write_file("tests/serf/test_calibration.py", test_body)
await cap.write_file("dossier/510k/section_4_calibration.md", "# Section 4\n…")
await cap.edit_file("src/opm_meg/__init__.py", old_snippet, new_snippet)
await cap.insert_lines("src/opm_meg/serf/__init__.py", after_line=0, content=import_line)
Each call returns a ProjectArtifactAuthoredPayload with action_kind, affected_paths, commit_sha, pre_commit_validation_results, session_id, authored_at.
Seven low-level actions¶
Action kinds come from PROJECT_ACTION_KINDS — the single source of truth for the set. The ProjectArtifactAuthoredPayload.action_kind Literal enumerates the same strings; a drift-detector test asserts they stay in sync.
| Method | Effect | Validators applied to |
|---|---|---|
write_file(path, content) |
Create or overwrite | resulting file |
edit_file(path, old_content, new_content) |
Unique-match string replace | resulting file |
delete_file(path) |
Remove | none (file is gone) |
move_file(src, dst) |
Rename | resulting file (dst) |
insert_lines(path, after_line, content) |
Insert (after_line=0 for prepend, =N for append) |
resulting file |
delete_lines(path, start_line, end_line) |
Drop inclusive 1-indexed range | resulting file |
replace_lines(path, start_line, end_line, content) |
Replace inclusive 1-indexed range | resulting file |
make_directory(path) |
Create a directory + .gitkeep placeholder so an empty dir round-trips through git |
none (placeholder is empty) |
remove_directory(path) |
Recursively remove a directory | none (everything is gone) |
copy_file(src, dst) |
Copy a file (dst must not exist) |
resulting file (dst) |
set_file_executable(path, executable) |
Toggle the POSIX executable bit on a file (e.g. for scripts under tools/); pure metadata change, no content edit |
none (chmod-only) |
Guiding Principle
(CPS_ALIGNMENT_PLAN.md) A finite, validatable set of low-level operations beats either (a) shell access or (b) a sprawling catalog of high-level / language-specific ops. Higher-level outcomes — "add a Python module with tests and a dossier section" — are sequences of these emitted by the planner; CPS-shaped planner prompts live in L2-G (PR 6).
Pipeline (every action)¶
The seven actions all funnel through one private helper _run_action_sync so the discipline can't drift between methods:
- Snapshot — capture pre-action content (or absence) of every affected path. The rollback record.
- Mutate — apply the in-process file operation.
- Validate — run every registered validator whose matcher matches the resulting path. All must pass.
- Rollback on failure — restore snapshots, then raise
DesignMonorepoErrorwith the validator'sdetail. - Commit + event —
client.commit_with_identity(identity, "L1-F <action_kind>: <primary_path>", paths=[...]), then emitProjectArtifactAuthoredon the blackboard.
Path safety¶
Every method validates its path arguments through _resolve_safe_path (in capabilities.py). It rejects:
- Absolute paths.
- Path-traversal (
..that escapesworking_dir). - Anything under
.colony/— that's L1-E's surface, route throughToolBuilder. - Anything under
.git/— git internals. - Empty paths.
The forbidden top-levels are declared once as _L1F_FORBIDDEN_TOP_LEVEL = frozenset({".colony", ".git"}) — single source of truth.
Validator registry¶
colony.design_monorepo.artifact_validators holds one ordered list of (matcher, validator) triples. The capability queries validators_for(path), runs every match, and aborts the commit on the first failure.
Built-in validators (DEFAULT_VALIDATORS):
| Validator | Matches | Source |
|---|---|---|
ast_allow_list |
.py under src/ or tests/ |
reuses ast_validator.py — same module L1-E goes through |
pytest_collect |
tests/**/*.py |
subprocess pytest --collect-only; passes when pytest is absent |
dossier_markdown |
dossier/**/*.md |
UTF-8 + non-empty + at least one heading |
ipynb_ast |
*.ipynb |
parses JSON cells; runs AST allow-list on each Python code cell |
cad_non_empty |
.step / .stp / .iges / .igs |
stub: non-empty file |
fea_input_non_empty |
.inp / .med |
stub: non-empty file |
reqif_non_empty |
.reqif |
stub: non-empty file |
CPS PR 6 (L2-G) plugs in real domain validators via register_validator(ArtifactValidator(...)). Multiple validators on the same path stack — all must pass.
from polymathera.colony.design_monorepo.artifact_validators import (
ArtifactValidator, register_validator,
)
def _validate_step(repo_root, rel):
# parse via OpenCascade / pythonocc-core
...
register_validator(ArtifactValidator(
name="cad_step_parser",
matcher=lambda p: p.suffix in {".step", ".stp"},
run=_validate_step,
))
The AST allow-list is the SAME module as L1-E. One uniform pipeline across both halves of L4 — Risk #5's stopgap defence.
Audit trail¶
Symmetric with L1-E's ExtensionAuthored:
from polymathera.colony.agents.blackboard.protocol import DesignMonorepoEventProtocol
key = DesignMonorepoEventProtocol.project_artifact_authored_key(
"write_file", "src/opm_meg/calibration.py",
)
entry = await bb.read(key)
# entry.value: dict matching ProjectArtifactAuthoredPayload, including
# pre_commit_validation_results for the audit log.
Subscribe with the wildcard:
@event_handler(pattern=DesignMonorepoEventProtocol.project_artifact_authored_pattern())
async def _on_authored(self, event, repl) -> EventProcessingResult | None:
kind, primary_path = DesignMonorepoEventProtocol.parse_project_artifact_authored_key(event.key)
...
session_id is populated automatically from get_current_session_id(). user_message_id is reserved for a future provenance plumbing pass.
Read-side companions¶
RepoStateProvider (extended by this PR) exposes three planner-friendly read methods:
list_packages()— top-level Python packages undersrc/(directories with__init__.py).list_design_artifacts(kind=...)— files by kind:cad,fea,reqif,notebook,dossier,test,python_module.kind=Nonereturns everything.summarize_project_layout()—{"top_level": [...], "counts": {kind: n}}. The "what's already here" snapshot the planner reads on its first turn.
All three exclude .colony/ and .git/.
Boundary with SandboxedShellCapability¶
The shell capability ships actions named write_file / edit_file too — these operate inside the container (not the working tree) and are appropriate for:
- Debug prints written into a sandboxed scratch dir.
- Temporary fixtures the agent generates during exploration.
- Anything the agent intends to be thrown away at session end.
ProjectAuthoringCapability is the authoritative write surface for design substance — anything that should land in the monorepo, be committed under the agent's identity, and be visible to the next session. CPS coordinator planner guidance (L2-G) routes design-substance mutations through ProjectAuthoringCapability.
Risk #9 lint¶
colony.design_monorepo.session_lint ships lint_session_actions(actions: Iterable[dict]) -> list[LintFinding] that flags any SandboxedShellCapability.write_file / edit_file action targeting a design-artifact top-level directory (src/, tests/, dossier/, data/, models/, notebooks/, docs/). The top-levels are declared once as DESIGN_ARTIFACT_TOP_LEVELS — single source of truth.
Findings are pure data; CI wraps the function and either fails the build or surfaces them as warnings. Conservative by design — only flags what we can identify with confidence.
What's not in scope for PR 3¶
- Per-domain validators (CAD parser, FEA solver-input parser, ReqIF schema, dossier markdown lint) — PR 6 plugs these in via
register_validator(...). PR 3 ships the registry and non-empty stubs. - CPS-shaped scaffolds for project substance (
project_layout,module_*,artifact_*) — PR 6. - Per-domain planner prompts that route requests through these actions — PR 6.
- Wiring the lint into a
polymathCLI subcommand — defer until a session-transcript schema is finalised.