@pennixrv/trellis 0.6.18 → 0.6.20

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/dist/commands/channel/adapters/codex.d.ts +14 -0
  2. package/dist/commands/channel/adapters/codex.d.ts.map +1 -1
  3. package/dist/commands/channel/adapters/codex.js +37 -0
  4. package/dist/commands/channel/adapters/codex.js.map +1 -1
  5. package/dist/commands/channel/adapters/index.d.ts.map +1 -1
  6. package/dist/commands/channel/adapters/index.js +49 -13
  7. package/dist/commands/channel/adapters/index.js.map +1 -1
  8. package/dist/commands/channel/index.d.ts.map +1 -1
  9. package/dist/commands/channel/index.js +17 -1
  10. package/dist/commands/channel/index.js.map +1 -1
  11. package/dist/commands/channel/wait.d.ts +4 -0
  12. package/dist/commands/channel/wait.d.ts.map +1 -1
  13. package/dist/commands/channel/wait.js +8 -1
  14. package/dist/commands/channel/wait.js.map +1 -1
  15. package/dist/migrations/manifests/0.6.19.json +9 -0
  16. package/dist/migrations/manifests/0.6.20.json +9 -0
  17. package/dist/templates/common/bundled-skills/trellis-channel/SKILL.md +2 -0
  18. package/dist/templates/common/bundled-skills/trellis-channel/references/command-reference.md +35 -5
  19. package/dist/templates/common/bundled-skills/trellis-channel/references/progress-debugging.md +24 -13
  20. package/dist/templates/common/bundled-skills/trellis-channel/references/subnode-work.md +159 -0
  21. package/dist/templates/common/bundled-skills/trellis-channel/references/workflows.md +6 -3
  22. package/dist/templates/shared-hooks/inject-workflow-state.py +17 -16
  23. package/dist/templates/trellis/agents/subnode.md +62 -0
  24. package/dist/templates/trellis/config.yaml +9 -9
  25. package/dist/templates/trellis/index.d.ts +3 -0
  26. package/dist/templates/trellis/index.d.ts.map +1 -1
  27. package/dist/templates/trellis/index.js +6 -0
  28. package/dist/templates/trellis/index.js.map +1 -1
  29. package/dist/templates/trellis/scripts/common/config.py +5 -4
  30. package/dist/templates/trellis/scripts/common/task_store.py +3 -3
  31. package/dist/templates/trellis/scripts/common/workflow_phase.py +1 -1
  32. package/dist/templates/trellis/scripts/subnode_artifact.py +498 -0
  33. package/dist/templates/trellis/scripts/workspace_note.py +126 -0
  34. package/dist/templates/trellis/workflow.md +5 -5
  35. package/package.json +2 -2
@@ -56,6 +56,8 @@ export const initDeveloperScript = readTemplate("scripts/init_developer.py");
56
56
  export const taskScript = readTemplate("scripts/task.py");
57
57
  export const getContextScript = readTemplate("scripts/get_context.py");
58
58
  export const addSessionScript = readTemplate("scripts/add_session.py");
59
+ export const subnodeArtifactScript = readTemplate("scripts/subnode_artifact.py");
60
+ export const workspaceNoteScript = readTemplate("scripts/workspace_note.py");
59
61
  // Configuration files
60
62
  export const workflowMdTemplate = readTemplate("workflow.md");
61
63
  export const configYamlTemplate = readTemplate("config.yaml");
@@ -67,6 +69,7 @@ export const gitattributesTemplate = readTemplate("gitattributes.txt");
67
69
  // and refreshed by `trellis update`.
68
70
  export const implementAgentTemplate = readTemplate("agents/implement.md");
69
71
  export const checkAgentTemplate = readTemplate("agents/check.md");
72
+ export const subnodeAgentTemplate = readTemplate("agents/subnode.md");
70
73
  /**
71
74
  * Get all script templates as a map of relative path to content
72
75
  */
@@ -102,6 +105,8 @@ export function getAllScripts() {
102
105
  scripts.set("task.py", taskScript);
103
106
  scripts.set("get_context.py", getContextScript);
104
107
  scripts.set("add_session.py", addSessionScript);
108
+ scripts.set("subnode_artifact.py", subnodeArtifactScript);
109
+ scripts.set("workspace_note.py", workspaceNoteScript);
105
110
  return scripts;
106
111
  }
107
112
  /**
@@ -116,6 +121,7 @@ export function getAllAgents() {
116
121
  const agents = new Map();
117
122
  agents.set("implement.md", implementAgentTemplate);
118
123
  agents.set("check.md", checkAgentTemplate);
124
+ agents.set("subnode.md", subnodeAgentTemplate);
119
125
  return agents;
120
126
  }
121
127
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/templates/trellis/index.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/templates/trellis/index.ts"],"names":[],"mappings":"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"}
@@ -22,7 +22,7 @@ from .trellis_config import parse_simple_yaml
22
22
  DEFAULT_SESSION_COMMIT_MESSAGE = "chore: record journal"
23
23
  DEFAULT_MAX_JOURNAL_LINES = 2000
24
24
  DEFAULT_SESSION_AUTO_COMMIT = True
25
- DEFAULT_CODEX_DISPATCH_MODE = "auto"
25
+ DEFAULT_CODEX_DISPATCH_MODE = "inline"
26
26
 
27
27
  CONFIG_FILE = "config.yaml"
28
28
 
@@ -140,9 +140,10 @@ def get_session_auto_commit(repo_root: Path | None = None) -> bool:
140
140
  def get_codex_dispatch_mode(repo_root: Path | None = None) -> str:
141
141
  """Return Codex dispatch mode.
142
142
 
143
- Default is ``auto``, which dispatches Trellis sub-agents and uses native
144
- context injection with a child-side fallback. ``inline`` is an explicit
145
- opt-out. ``sub-agent`` remains a backwards-compatible alias for ``auto``.
143
+ Default is ``inline``: the main session carries out implementation and
144
+ verification directly. ``auto`` explicitly enables Trellis sub-agents and
145
+ native context injection with a child-side fallback. ``sub-agent`` remains
146
+ a backwards-compatible alias for ``auto``.
146
147
 
147
148
  Invalid explicit configuration falls back to ``inline`` rather than
148
149
  unexpectedly dispatching a sub-agent. This CLI-facing parser is the only
@@ -230,9 +230,9 @@ def _has_subagent_platform(repo_root: Path) -> bool:
230
230
  """Return True if any sub-agent-capable platform is configured.
231
231
 
232
232
  Detected by probing well-known config directories at the repo root. Codex
233
- counts by default through ``codex.dispatch_mode: auto`` (including the
234
- legacy ``sub-agent`` alias); explicit inline mode loads context through
235
- skills, not JSONL.
233
+ counts only when ``codex.dispatch_mode: auto`` is explicitly selected
234
+ (including the legacy ``sub-agent`` alias); the default inline mode loads
235
+ context through skills, not JSONL.
236
236
  """
237
237
  for config_dir in _SUBAGENT_CONFIG_DIRS:
238
238
  if (repo_root / config_dir).is_dir():
@@ -181,7 +181,7 @@ def resolve_effective_platform(platform: str, config: dict) -> str:
181
181
  if label:
182
182
  return label
183
183
  if platform == "codex":
184
- mode = "auto"
184
+ mode = "inline"
185
185
  codex_cfg = config.get("codex") if isinstance(config, dict) else None
186
186
  if codex_cfg is not None:
187
187
  if not isinstance(codex_cfg, dict):
@@ -0,0 +1,498 @@
1
+ #!/usr/bin/env python3
2
+ """Create and validate durable artifacts for one bounded Trellis subnode.
3
+
4
+ The coordinator creates an immutable brief before dispatching a subnode. The
5
+ subnode appends its worklog and writes its own final report. This helper never
6
+ dispatches workers, decides acceptance, or mutates task state.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import argparse
12
+ import json
13
+ import re
14
+ import sys
15
+ from pathlib import Path
16
+ from typing import Any, NoReturn
17
+
18
+ from common.io import write_text_atomic
19
+ from common.paths import get_repo_root, get_tasks_dir
20
+ from common.task_utils import is_within_tasks_dir, resolve_task_dir
21
+ from common.tasks import load_task
22
+
23
+
24
+ SCHEMA_VERSION = 1
25
+ MAX_DRAFT_BYTES = 64 * 1024
26
+ MAX_REPORT_BYTES = 128 * 1024
27
+ ID_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$")
28
+ SECRET_PATTERNS = (
29
+ re.compile(r"-----BEGIN [A-Z ]*PRIVATE KEY-----"),
30
+ re.compile(r"\bsk-[A-Za-z0-9]{20,}\b"),
31
+ re.compile(r"\bgh[pousr]_[A-Za-z0-9]{20,}\b"),
32
+ )
33
+
34
+
35
+ class ArtifactError(Exception):
36
+ """A user-correctable artifact validation failure."""
37
+
38
+
39
+ def _fail(message: str) -> NoReturn:
40
+ raise ArtifactError(message)
41
+
42
+
43
+ def _read_json_object(path: Path, max_bytes: int, label: str) -> tuple[dict[str, Any], bytes]:
44
+ """Read one bounded, non-symlink JSON object."""
45
+ if path.is_symlink():
46
+ _fail(f"refusing symlinked {label}: {path}")
47
+ try:
48
+ size = path.stat().st_size
49
+ except OSError as exc:
50
+ _fail(f"could not stat {label} {path}: {exc}")
51
+ if size > max_bytes:
52
+ _fail(f"{label} exceeds the {max_bytes} byte limit: {path}")
53
+ try:
54
+ raw = path.read_bytes()
55
+ data = json.loads(raw.decode("utf-8"))
56
+ except FileNotFoundError:
57
+ _fail(f"{label} does not exist: {path}")
58
+ except UnicodeDecodeError:
59
+ _fail(f"{label} is not UTF-8: {path}")
60
+ except json.JSONDecodeError as exc:
61
+ _fail(f"{label} is not valid JSON: {path}: {exc.msg}")
62
+ except OSError as exc:
63
+ _fail(f"could not read {label} {path}: {exc}")
64
+ if not isinstance(data, dict):
65
+ _fail(f"{label} must contain a JSON object: {path}")
66
+ return data, raw
67
+
68
+
69
+ def _require_text(value: Any, field: str, *, max_len: int = 4096) -> str:
70
+ if not isinstance(value, str) or not value.strip():
71
+ _fail(f"{field} must be a non-empty string")
72
+ if len(value) > max_len or "\x00" in value:
73
+ _fail(f"{field} is not a bounded text value")
74
+ return value
75
+
76
+
77
+ def _require_text_list(value: Any, field: str, *, allow_empty: bool = False) -> list[str]:
78
+ if not isinstance(value, list) or (not value and not allow_empty):
79
+ _fail(f"{field} must be {'a list' if allow_empty else 'a non-empty list'}")
80
+ result: list[str] = []
81
+ for index, item in enumerate(value):
82
+ result.append(_require_text(item, f"{field}[{index}]"))
83
+ return result
84
+
85
+
86
+ def _require_id(value: Any, field: str) -> str:
87
+ text = _require_text(value, field, max_len=64)
88
+ if not ID_RE.fullmatch(text):
89
+ _fail(f"{field} must match {ID_RE.pattern}")
90
+ return text
91
+
92
+
93
+ def _reject_obvious_secrets(raw: bytes, label: str) -> None:
94
+ text = raw.decode("utf-8", errors="replace")
95
+ for pattern in SECRET_PATTERNS:
96
+ if pattern.search(text):
97
+ _fail(f"{label} appears to contain a credential; remove it before writing the artifact")
98
+
99
+
100
+ def _task_id(task_dir: Path) -> str:
101
+ task = load_task(task_dir)
102
+ if task is None:
103
+ _fail(f"task.json is missing or invalid: {task_dir}")
104
+ task_id = task.raw.get("id") or task.dir_name
105
+ if not isinstance(task_id, str) or not task_id.strip():
106
+ _fail(f"task has no usable id: {task_dir}")
107
+ if task.status not in {"planning", "in_progress"}:
108
+ _fail(f"task is not active (planning or in_progress): {task_dir}")
109
+ return task_id
110
+
111
+
112
+ def _task_lexical_from_resolved(task_resolved: Path, repo_root: Path) -> Path:
113
+ tasks_lexical = get_tasks_dir(repo_root)
114
+ try:
115
+ relative = task_resolved.relative_to(tasks_lexical.resolve())
116
+ except ValueError:
117
+ _fail(f"task is outside the Trellis task directory: {task_resolved}")
118
+ return tasks_lexical / relative
119
+
120
+
121
+ def _assert_no_subpath_symlinks(task_dir: Path, target: Path) -> None:
122
+ """Reject symlinks below a task while permitting a symlinked .trellis root."""
123
+ if task_dir.is_symlink():
124
+ _fail(f"refusing symlinked task directory: {task_dir}")
125
+ try:
126
+ parts = target.relative_to(task_dir).parts
127
+ except ValueError:
128
+ _fail(f"artifact path is outside its task: {target}")
129
+ current = task_dir
130
+ for part in parts:
131
+ current = current / part
132
+ if current.is_symlink():
133
+ _fail(f"refusing symlinked artifact path component: {current}")
134
+
135
+
136
+ def _node_dir(task_dir: Path, work_id: str, subnode_id: str) -> Path:
137
+ return task_dir / "subnodes" / work_id / subnode_id
138
+
139
+
140
+ def _relative_to_repo(path: Path, repo_root: Path) -> str:
141
+ try:
142
+ return path.relative_to(repo_root).as_posix()
143
+ except ValueError:
144
+ _fail(f"path is outside repository: {path}")
145
+
146
+
147
+ def _validate_snapshot(value: Any) -> None:
148
+ if not isinstance(value, list) or not value:
149
+ _fail("source_snapshot must be a non-empty list")
150
+ for index, item in enumerate(value):
151
+ if not isinstance(item, dict):
152
+ _fail(f"source_snapshot[{index}] must be an object")
153
+ _require_text(item.get("locator"), f"source_snapshot[{index}].locator")
154
+ _require_text(
155
+ item.get("revision_or_digest"),
156
+ f"source_snapshot[{index}].revision_or_digest",
157
+ )
158
+ _require_text(item.get("observed_at"), f"source_snapshot[{index}].observed_at")
159
+
160
+
161
+ def _validate_channel_ref(value: Any) -> None:
162
+ if not isinstance(value, dict):
163
+ _fail("channel_ref must be an object")
164
+ _require_text(value.get("name"), "channel_ref.name", max_len=128)
165
+ scope = _require_text(value.get("scope"), "channel_ref.scope", max_len=32)
166
+ if scope not in {"project", "global"}:
167
+ _fail("channel_ref.scope must be project or global")
168
+ _require_text(value.get("worker_handle"), "channel_ref.worker_handle", max_len=128)
169
+
170
+
171
+ def _validate_retry_target(
172
+ retry_of: Any,
173
+ task_dir: Path,
174
+ work_id: str,
175
+ subnode_id: str,
176
+ ) -> str | None:
177
+ """Validate the coordinator's explicit, already-created retry source."""
178
+ if retry_of is None:
179
+ return None
180
+ retry_id = _require_id(retry_of, "brief.retry_of")
181
+ if retry_id == subnode_id:
182
+ _fail("brief.retry_of must name a different subnode")
183
+ target_dir = _node_dir(task_dir, work_id, retry_id)
184
+ _assert_no_subpath_symlinks(task_dir, target_dir)
185
+ target_brief_path = target_dir / "brief.json"
186
+ if not target_brief_path.exists():
187
+ _fail("brief.retry_of must identify an existing subnode brief in this task and work_id")
188
+ target_brief, _raw = _read_json_object(
189
+ target_brief_path,
190
+ MAX_DRAFT_BYTES,
191
+ "brief.retry_of target brief",
192
+ )
193
+ expected_task_id = _task_id(task_dir)
194
+ if target_brief.get("schema_version") != SCHEMA_VERSION:
195
+ _fail("brief.retry_of target brief has an unsupported schema_version")
196
+ if target_brief.get("task_id") != expected_task_id:
197
+ _fail("brief.retry_of target brief does not belong to this task")
198
+ if target_brief.get("work_id") != work_id:
199
+ _fail("brief.retry_of target brief does not belong to this work_id")
200
+ if target_brief.get("subnode_id") != retry_id:
201
+ _fail("brief.retry_of target brief identity does not match its artifact directory")
202
+ if target_brief.get("role_id") != "subnode":
203
+ _fail("brief.retry_of target brief.role_id must be subnode")
204
+ return retry_id
205
+
206
+
207
+ def _validate_brief_data(
208
+ brief: dict[str, Any],
209
+ task_dir: Path,
210
+ node_dir: Path,
211
+ repo_root: Path,
212
+ ) -> None:
213
+ expected_task_id = _task_id(task_dir)
214
+ if brief.get("schema_version") != SCHEMA_VERSION:
215
+ _fail(f"brief.schema_version must be {SCHEMA_VERSION}")
216
+ if brief.get("task_id") != expected_task_id:
217
+ _fail("brief.task_id does not match task.json")
218
+ work_id = _require_id(brief.get("work_id"), "brief.work_id")
219
+ subnode_id = _require_id(brief.get("subnode_id"), "brief.subnode_id")
220
+ if node_dir != _node_dir(task_dir, work_id, subnode_id):
221
+ _fail("brief identity does not match its artifact directory")
222
+ if brief.get("role_id") != "subnode":
223
+ _fail("brief.role_id must be subnode")
224
+ _require_text(brief.get("question"), "brief.question")
225
+ _require_text(brief.get("independence_reason"), "brief.independence_reason")
226
+ _require_text_list(brief.get("scope"), "brief.scope")
227
+ _require_text_list(brief.get("protected_targets"), "brief.protected_targets", allow_empty=True)
228
+ _require_text(brief.get("lens"), "brief.lens")
229
+ _require_text(brief.get("evidence_method"), "brief.evidence_method")
230
+ _validate_snapshot(brief.get("source_snapshot"))
231
+ _require_text_list(brief.get("dependencies"), "brief.dependencies", allow_empty=True)
232
+ _require_text_list(brief.get("stop_conditions"), "brief.stop_conditions")
233
+ _require_text(brief.get("deadline"), "brief.deadline", max_len=128)
234
+ _validate_channel_ref(brief.get("channel_ref"))
235
+ if "retry_of" not in brief:
236
+ _fail("brief.retry_of must be present; use null when this is not a retry")
237
+ if "counter_of" not in brief:
238
+ _fail("brief.counter_of must be present; use null when this is not counterwork")
239
+ _validate_retry_target(
240
+ brief["retry_of"],
241
+ task_dir,
242
+ work_id,
243
+ subnode_id,
244
+ )
245
+ counter_of = brief["counter_of"]
246
+ if counter_of is not None:
247
+ counter_id = _require_id(counter_of, "brief.counter_of")
248
+ if counter_id == subnode_id:
249
+ _fail("brief.counter_of must name a different subnode")
250
+ expected_report = _relative_to_repo(node_dir / "report.json", repo_root)
251
+ if brief.get("report_path") != expected_report:
252
+ _fail("brief.report_path does not match its immutable artifact directory")
253
+
254
+
255
+ def _resolve_artifact_file(
256
+ value: str,
257
+ expected_name: str,
258
+ repo_root: Path,
259
+ ) -> tuple[Path, Path, Path]:
260
+ """Return lexical artifact, node directory, and task directory after checks."""
261
+ supplied = Path(value)
262
+ lexical = supplied if supplied.is_absolute() else repo_root / supplied
263
+ if lexical.name != expected_name:
264
+ _fail(f"expected a {expected_name} path, received: {value}")
265
+ if lexical.is_symlink():
266
+ _fail(f"refusing symlinked artifact: {lexical}")
267
+ try:
268
+ resolved = lexical.resolve(strict=True)
269
+ except (OSError, RuntimeError) as exc:
270
+ _fail(f"could not resolve artifact {lexical}: {exc}")
271
+ node_resolved = resolved.parent
272
+ work_resolved = node_resolved.parent
273
+ subnodes_resolved = work_resolved.parent
274
+ task_resolved = subnodes_resolved.parent
275
+ if subnodes_resolved.name != "subnodes":
276
+ _fail(f"artifact is not under a task subnodes directory: {lexical}")
277
+ if not is_within_tasks_dir(task_resolved, repo_root):
278
+ _fail(f"artifact task is not a direct active task: {lexical}")
279
+ task_dir = _task_lexical_from_resolved(task_resolved, repo_root)
280
+ expected = _node_dir(task_dir, work_resolved.name, node_resolved.name) / expected_name
281
+ try:
282
+ same_path = lexical.resolve() == expected.resolve()
283
+ except (OSError, RuntimeError) as exc:
284
+ _fail(f"could not verify artifact containment: {exc}")
285
+ if not same_path:
286
+ _fail(f"artifact path does not match its task-owned location: {lexical}")
287
+ _assert_no_subpath_symlinks(task_dir, expected)
288
+ return expected, expected.parent, task_dir
289
+
290
+
291
+ def _validate_brief_file(
292
+ value: str,
293
+ repo_root: Path,
294
+ ) -> tuple[dict[str, Any], bytes, Path, Path, Path]:
295
+ brief_path, node_dir, task_dir = _resolve_artifact_file(value, "brief.json", repo_root)
296
+ brief, raw = _read_json_object(brief_path, MAX_DRAFT_BYTES, "brief")
297
+ _reject_obvious_secrets(raw, "brief")
298
+ _validate_brief_data(brief, task_dir, node_dir, repo_root)
299
+ return brief, raw, brief_path, node_dir, task_dir
300
+
301
+
302
+ def _validate_evidence(value: Any) -> set[tuple[str, str]]:
303
+ if not isinstance(value, list):
304
+ _fail("report.evidence must be a list")
305
+ identities: set[tuple[str, str]] = set()
306
+ for index, item in enumerate(value):
307
+ if not isinstance(item, dict):
308
+ _fail(f"report.evidence[{index}] must be an object")
309
+ evidence_id = _require_text(item.get("id"), f"report.evidence[{index}].id", max_len=128)
310
+ locator = _require_text(item.get("locator"), f"report.evidence[{index}].locator")
311
+ _require_text(item.get("summary"), f"report.evidence[{index}].summary")
312
+ identity = (evidence_id, locator)
313
+ if identity in identities:
314
+ _fail("report.evidence contains a duplicate evidence identity")
315
+ identities.add(identity)
316
+ return identities
317
+
318
+
319
+ def _validate_report_data(
320
+ report: dict[str, Any],
321
+ brief: dict[str, Any],
322
+ ) -> set[tuple[str, str]]:
323
+ if report.get("schema_version") != SCHEMA_VERSION:
324
+ _fail(f"report.schema_version must be {SCHEMA_VERSION}")
325
+ for field in ("task_id", "work_id", "subnode_id", "role_id"):
326
+ if report.get(field) != brief.get(field):
327
+ _fail(f"report.{field} does not match brief.{field}")
328
+ if report.get("scope") != brief.get("scope"):
329
+ _fail("report.scope does not match brief.scope")
330
+ if report.get("lens") != brief.get("lens"):
331
+ _fail("report.lens does not match brief.lens")
332
+ status = report.get("status")
333
+ if status not in {"complete", "blocked", "incomplete", "error"}:
334
+ _fail("report.status must be complete, blocked, incomplete, or error")
335
+ evidence = _validate_evidence(report.get("evidence"))
336
+ for field in ("findings", "uncertainties", "corrections"):
337
+ if not isinstance(report.get(field), list):
338
+ _fail(f"report.{field} must be a list")
339
+ if status == "complete" and not evidence:
340
+ _fail("a complete report requires at least one evidence item")
341
+ if status != "complete":
342
+ _require_text_list(report.get("completed_scope"), "report.completed_scope")
343
+ _require_text(report.get("blocker"), "report.blocker")
344
+ return evidence
345
+
346
+
347
+ def _validate_report_file(
348
+ value: str,
349
+ repo_root: Path,
350
+ ) -> tuple[dict[str, Any], set[tuple[str, str]], dict[str, Any], Path]:
351
+ report_path, node_dir, _task_dir = _resolve_artifact_file(value, "report.json", repo_root)
352
+ brief_path = node_dir / "brief.json"
353
+ brief, _brief_raw, _brief_path, _brief_node, _brief_task = _validate_brief_file(
354
+ str(brief_path), repo_root
355
+ )
356
+ expected_report = _relative_to_repo(report_path, repo_root)
357
+ if brief.get("report_path") != expected_report:
358
+ _fail("brief.report_path does not point to the report being validated")
359
+ report, raw = _read_json_object(report_path, MAX_REPORT_BYTES, "report")
360
+ _reject_obvious_secrets(raw, "report")
361
+ evidence = _validate_report_data(report, brief)
362
+ return report, evidence, brief, node_dir
363
+
364
+
365
+ def _init(args: argparse.Namespace) -> None:
366
+ repo_root = get_repo_root()
367
+ task_dir = resolve_task_dir(args.task, repo_root)
368
+ if task_dir is None or not is_within_tasks_dir(task_dir, repo_root):
369
+ _fail("--task must identify a direct active task directory")
370
+ if task_dir.is_symlink():
371
+ _fail(f"refusing symlinked task directory: {task_dir}")
372
+ work_id = _require_id(args.work_id, "--work-id")
373
+ subnode_id = _require_id(args.subnode_id, "--subnode-id")
374
+ node_dir = _node_dir(task_dir, work_id, subnode_id)
375
+ _assert_no_subpath_symlinks(task_dir, node_dir)
376
+ if node_dir.exists() or node_dir.is_symlink():
377
+ _fail(f"subnode artifacts already exist: {node_dir}")
378
+ draft_path = Path(args.draft)
379
+ if not draft_path.is_absolute():
380
+ draft_path = repo_root / draft_path
381
+ draft, raw = _read_json_object(draft_path, MAX_DRAFT_BYTES, "brief draft")
382
+ _reject_obvious_secrets(raw, "brief draft")
383
+ expected_task_id = _task_id(task_dir)
384
+ brief = dict(draft)
385
+ brief["schema_version"] = SCHEMA_VERSION
386
+ brief["task_id"] = expected_task_id
387
+ brief["work_id"] = work_id
388
+ brief["subnode_id"] = subnode_id
389
+ brief["role_id"] = "subnode"
390
+ brief["report_path"] = _relative_to_repo(node_dir / "report.json", repo_root)
391
+ _validate_brief_data(brief, task_dir, node_dir, repo_root)
392
+ for directory in (task_dir / "subnodes", task_dir / "subnodes" / work_id, node_dir):
393
+ if directory.is_symlink():
394
+ _fail(f"refusing symlinked artifact directory: {directory}")
395
+ if directory.exists() and not directory.is_dir():
396
+ _fail(f"artifact path component is not a directory: {directory}")
397
+ try:
398
+ directory.mkdir(exist_ok=True)
399
+ except OSError as exc:
400
+ _fail(f"could not create artifact directory {directory}: {exc}")
401
+ brief_path = node_dir / "brief.json"
402
+ worklog_path = node_dir / "worklog.md"
403
+ brief_text = json.dumps(brief, indent=2, ensure_ascii=False) + "\n"
404
+ worklog_text = (
405
+ "# Subnode Worklog\n\n"
406
+ f"- Task: `{brief['task_id']}`\n"
407
+ f"- Work: `{work_id}`\n"
408
+ f"- Subnode: `{subnode_id}`\n\n"
409
+ "Append substantive observations, corrections, blockers, and completion notes below.\n"
410
+ )
411
+ if not write_text_atomic(brief_path, brief_text):
412
+ _fail(f"could not write immutable brief: {brief_path}")
413
+ if not write_text_atomic(worklog_path, worklog_text):
414
+ _fail(f"could not write subnode worklog: {worklog_path}")
415
+ print(json.dumps({
416
+ "brief": _relative_to_repo(brief_path, repo_root),
417
+ "worklog": _relative_to_repo(worklog_path, repo_root),
418
+ "report": brief["report_path"],
419
+ }))
420
+
421
+
422
+ def _validate(args: argparse.Namespace) -> None:
423
+ repo_root = get_repo_root()
424
+ report, _evidence, _brief, _node_dir_value = _validate_report_file(args.report, repo_root)
425
+ print(f"Validated pending-review report: {report['subnode_id']}")
426
+
427
+
428
+ def _validate_counter(args: argparse.Namespace) -> None:
429
+ repo_root = get_repo_root()
430
+ primary_dir = Path(args.primary)
431
+ counter_dir = Path(args.counter)
432
+ if not primary_dir.is_absolute():
433
+ primary_dir = repo_root / primary_dir
434
+ if not counter_dir.is_absolute():
435
+ counter_dir = repo_root / counter_dir
436
+ primary_report, primary_evidence, primary_brief, primary_node = _validate_report_file(
437
+ str(primary_dir / "report.json"), repo_root
438
+ )
439
+ counter_report, counter_evidence, counter_brief, counter_node = _validate_report_file(
440
+ str(counter_dir / "report.json"), repo_root
441
+ )
442
+ if primary_node == counter_node:
443
+ _fail("counter validation requires two different subnode directories")
444
+ if primary_brief["task_id"] != counter_brief["task_id"]:
445
+ _fail("counter subnode must belong to the same task")
446
+ if primary_brief["work_id"] != counter_brief["work_id"]:
447
+ _fail("counter subnode must belong to the same work_id")
448
+ if counter_brief.get("counter_of") != primary_brief["subnode_id"]:
449
+ _fail("counter brief.counter_of must identify the primary subnode")
450
+ if primary_brief["lens"] == counter_brief["lens"]:
451
+ _fail("counter subnode must use a different lens")
452
+ if primary_report["status"] != "complete" or counter_report["status"] != "complete":
453
+ _fail("counter comparison requires two complete reports")
454
+ if primary_evidence & counter_evidence:
455
+ _fail("counter report must use evidence identities independent of the primary report")
456
+ print(
457
+ "Validated independent counter reports: "
458
+ f"{primary_brief['subnode_id']} and {counter_brief['subnode_id']}"
459
+ )
460
+
461
+
462
+ def _parser() -> argparse.ArgumentParser:
463
+ parser = argparse.ArgumentParser(description=__doc__)
464
+ subparsers = parser.add_subparsers(dest="command", required=True)
465
+
466
+ init = subparsers.add_parser("init", help="create immutable brief and empty worklog")
467
+ init.add_argument("--task", required=True, help="direct active Trellis task directory")
468
+ init.add_argument("--work-id", required=True)
469
+ init.add_argument("--subnode-id", required=True)
470
+ init.add_argument("--draft", required=True, help="coordinator-owned brief draft JSON")
471
+ init.set_defaults(handler=_init)
472
+
473
+ validate = subparsers.add_parser("validate", help="validate a pending-review report")
474
+ validate.add_argument("--report", required=True)
475
+ validate.set_defaults(handler=_validate)
476
+
477
+ counter = subparsers.add_parser(
478
+ "validate-counter",
479
+ help="validate two completed, independent subnode directories",
480
+ )
481
+ counter.add_argument("--primary", required=True, help="primary subnode artifact directory")
482
+ counter.add_argument("--counter", required=True, help="counter subnode artifact directory")
483
+ counter.set_defaults(handler=_validate_counter)
484
+ return parser
485
+
486
+
487
+ def main() -> int:
488
+ args = _parser().parse_args()
489
+ try:
490
+ args.handler(args)
491
+ except ArtifactError as exc:
492
+ print(f"Error: {exc}", file=sys.stderr)
493
+ return 1
494
+ return 0
495
+
496
+
497
+ if __name__ == "__main__":
498
+ sys.exit(main())