@pennixrv/trellis 0.6.18 → 0.6.19
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.
- package/dist/commands/channel/adapters/codex.d.ts +14 -0
- package/dist/commands/channel/adapters/codex.d.ts.map +1 -1
- package/dist/commands/channel/adapters/codex.js +37 -0
- package/dist/commands/channel/adapters/codex.js.map +1 -1
- package/dist/commands/channel/adapters/index.d.ts.map +1 -1
- package/dist/commands/channel/adapters/index.js +49 -13
- package/dist/commands/channel/adapters/index.js.map +1 -1
- package/dist/templates/common/bundled-skills/trellis-channel/SKILL.md +2 -0
- package/dist/templates/common/bundled-skills/trellis-channel/references/subnode-work.md +143 -0
- package/dist/templates/shared-hooks/inject-workflow-state.py +17 -16
- package/dist/templates/trellis/agents/subnode.md +63 -0
- package/dist/templates/trellis/config.yaml +9 -9
- package/dist/templates/trellis/index.d.ts +3 -0
- package/dist/templates/trellis/index.d.ts.map +1 -1
- package/dist/templates/trellis/index.js +6 -0
- package/dist/templates/trellis/index.js.map +1 -1
- package/dist/templates/trellis/scripts/common/config.py +5 -4
- package/dist/templates/trellis/scripts/common/task_store.py +3 -3
- package/dist/templates/trellis/scripts/common/workflow_phase.py +1 -1
- package/dist/templates/trellis/scripts/subnode_artifact.py +463 -0
- package/dist/templates/trellis/scripts/workspace_note.py +126 -0
- package/dist/templates/trellis/workflow.md +5 -5
- package/package.json +2 -2
|
@@ -45,12 +45,15 @@ export declare const initDeveloperScript: string;
|
|
|
45
45
|
export declare const taskScript: string;
|
|
46
46
|
export declare const getContextScript: string;
|
|
47
47
|
export declare const addSessionScript: string;
|
|
48
|
+
export declare const subnodeArtifactScript: string;
|
|
49
|
+
export declare const workspaceNoteScript: string;
|
|
48
50
|
export declare const workflowMdTemplate: string;
|
|
49
51
|
export declare const configYamlTemplate: string;
|
|
50
52
|
export declare const gitignoreTemplate: string;
|
|
51
53
|
export declare const gitattributesTemplate: string;
|
|
52
54
|
export declare const implementAgentTemplate: string;
|
|
53
55
|
export declare const checkAgentTemplate: string;
|
|
56
|
+
export declare const subnodeAgentTemplate: string;
|
|
54
57
|
/**
|
|
55
58
|
* Get all script templates as a map of relative path to content
|
|
56
59
|
*/
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/templates/trellis/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAcH,eAAO,MAAM,WAAW,QAAsC,CAAC;AAG/D,eAAO,MAAM,UAAU,QAA6C,CAAC;AACrE,eAAO,MAAM,WAAW,QAA0C,CAAC;AACnE,eAAO,MAAM,eAAe,QAA8C,CAAC;AAC3E,eAAO,MAAM,gBAAgB,QAAgD,CAAC;AAC9E,eAAO,MAAM,eAAe,QAA+C,CAAC;AAC5E,eAAO,MAAM,eAAe,QAA+C,CAAC;AAC5E,eAAO,MAAM,gBAAgB,QAAgD,CAAC;AAC9E,eAAO,MAAM,gBAAgB,QAAgD,CAAC;AAC9E,eAAO,MAAM,YAAY,QAA2C,CAAC;AACrE,eAAO,MAAM,QAAQ,QAAuC,CAAC;AAC7D,eAAO,MAAM,SAAS,QAAwC,CAAC;AAC/D,eAAO,MAAM,SAAS,QAAwC,CAAC;AAC/D,eAAO,MAAM,WAAW,QAA0C,CAAC;AACnE,eAAO,MAAM,WAAW,QAA0C,CAAC;AACnE,eAAO,MAAM,iBAAiB,QAAiD,CAAC;AAChF,eAAO,MAAM,eAAe,QAA+C,CAAC;AAC5E,eAAO,MAAM,oBAAoB,QAEhC,CAAC;AACF,eAAO,MAAM,qBAAqB,QAEjC,CAAC;AACF,eAAO,MAAM,mBAAmB,QAE/B,CAAC;AACF,eAAO,MAAM,mBAAmB,QAE/B,CAAC;AACF,eAAO,MAAM,gBAAgB,QAAgD,CAAC;AAG9E,eAAO,MAAM,kBAAkB,QAA2C,CAAC;AAC3E,eAAO,MAAM,mBAAmB,QAA4C,CAAC;AAC7E,eAAO,MAAM,UAAU,QAAkC,CAAC;AAC1D,eAAO,MAAM,gBAAgB,QAAyC,CAAC;AACvE,eAAO,MAAM,gBAAgB,QAAyC,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/templates/trellis/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAcH,eAAO,MAAM,WAAW,QAAsC,CAAC;AAG/D,eAAO,MAAM,UAAU,QAA6C,CAAC;AACrE,eAAO,MAAM,WAAW,QAA0C,CAAC;AACnE,eAAO,MAAM,eAAe,QAA8C,CAAC;AAC3E,eAAO,MAAM,gBAAgB,QAAgD,CAAC;AAC9E,eAAO,MAAM,eAAe,QAA+C,CAAC;AAC5E,eAAO,MAAM,eAAe,QAA+C,CAAC;AAC5E,eAAO,MAAM,gBAAgB,QAAgD,CAAC;AAC9E,eAAO,MAAM,gBAAgB,QAAgD,CAAC;AAC9E,eAAO,MAAM,YAAY,QAA2C,CAAC;AACrE,eAAO,MAAM,QAAQ,QAAuC,CAAC;AAC7D,eAAO,MAAM,SAAS,QAAwC,CAAC;AAC/D,eAAO,MAAM,SAAS,QAAwC,CAAC;AAC/D,eAAO,MAAM,WAAW,QAA0C,CAAC;AACnE,eAAO,MAAM,WAAW,QAA0C,CAAC;AACnE,eAAO,MAAM,iBAAiB,QAAiD,CAAC;AAChF,eAAO,MAAM,eAAe,QAA+C,CAAC;AAC5E,eAAO,MAAM,oBAAoB,QAEhC,CAAC;AACF,eAAO,MAAM,qBAAqB,QAEjC,CAAC;AACF,eAAO,MAAM,mBAAmB,QAE/B,CAAC;AACF,eAAO,MAAM,mBAAmB,QAE/B,CAAC;AACF,eAAO,MAAM,gBAAgB,QAAgD,CAAC;AAG9E,eAAO,MAAM,kBAAkB,QAA2C,CAAC;AAC3E,eAAO,MAAM,mBAAmB,QAA4C,CAAC;AAC7E,eAAO,MAAM,UAAU,QAAkC,CAAC;AAC1D,eAAO,MAAM,gBAAgB,QAAyC,CAAC;AACvE,eAAO,MAAM,gBAAgB,QAAyC,CAAC;AACvE,eAAO,MAAM,qBAAqB,QAEjC,CAAC;AACF,eAAO,MAAM,mBAAmB,QAA4C,CAAC;AAG7E,eAAO,MAAM,kBAAkB,QAA8B,CAAC;AAC9D,eAAO,MAAM,kBAAkB,QAA8B,CAAC;AAC9D,eAAO,MAAM,iBAAiB,QAAgC,CAAC;AAC/D,eAAO,MAAM,qBAAqB,QAAoC,CAAC;AAMvE,eAAO,MAAM,sBAAsB,QAAsC,CAAC;AAC1E,eAAO,MAAM,kBAAkB,QAAkC,CAAC;AAClE,eAAO,MAAM,oBAAoB,QAAoC,CAAC;AAEtE;;GAEG;AACH,wBAAgB,aAAa,IAAI,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAuCnD;AAED;;;;;;;GAOG;AACH,wBAAgB,YAAY,IAAI,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAMlD"}
|
|
@@ -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 = "
|
|
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 ``
|
|
144
|
-
|
|
145
|
-
|
|
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
|
|
234
|
-
legacy ``sub-agent`` alias);
|
|
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 = "
|
|
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,463 @@
|
|
|
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 hashlib
|
|
13
|
+
import json
|
|
14
|
+
import re
|
|
15
|
+
import sys
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
from typing import Any, NoReturn
|
|
18
|
+
|
|
19
|
+
from common.io import write_text_atomic
|
|
20
|
+
from common.paths import get_repo_root, get_tasks_dir
|
|
21
|
+
from common.task_utils import is_within_tasks_dir, resolve_task_dir
|
|
22
|
+
from common.tasks import load_task
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
SCHEMA_VERSION = 1
|
|
26
|
+
MAX_DRAFT_BYTES = 64 * 1024
|
|
27
|
+
MAX_REPORT_BYTES = 128 * 1024
|
|
28
|
+
ID_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$")
|
|
29
|
+
SECRET_PATTERNS = (
|
|
30
|
+
re.compile(r"-----BEGIN [A-Z ]*PRIVATE KEY-----"),
|
|
31
|
+
re.compile(r"\bsk-[A-Za-z0-9]{20,}\b"),
|
|
32
|
+
re.compile(r"\bgh[pousr]_[A-Za-z0-9]{20,}\b"),
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class ArtifactError(Exception):
|
|
37
|
+
"""A user-correctable artifact validation failure."""
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _fail(message: str) -> NoReturn:
|
|
41
|
+
raise ArtifactError(message)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _read_json_object(path: Path, max_bytes: int, label: str) -> tuple[dict[str, Any], bytes]:
|
|
45
|
+
"""Read one bounded, non-symlink JSON object."""
|
|
46
|
+
if path.is_symlink():
|
|
47
|
+
_fail(f"refusing symlinked {label}: {path}")
|
|
48
|
+
try:
|
|
49
|
+
size = path.stat().st_size
|
|
50
|
+
except OSError as exc:
|
|
51
|
+
_fail(f"could not stat {label} {path}: {exc}")
|
|
52
|
+
if size > max_bytes:
|
|
53
|
+
_fail(f"{label} exceeds the {max_bytes} byte limit: {path}")
|
|
54
|
+
try:
|
|
55
|
+
raw = path.read_bytes()
|
|
56
|
+
data = json.loads(raw.decode("utf-8"))
|
|
57
|
+
except FileNotFoundError:
|
|
58
|
+
_fail(f"{label} does not exist: {path}")
|
|
59
|
+
except UnicodeDecodeError:
|
|
60
|
+
_fail(f"{label} is not UTF-8: {path}")
|
|
61
|
+
except json.JSONDecodeError as exc:
|
|
62
|
+
_fail(f"{label} is not valid JSON: {path}: {exc.msg}")
|
|
63
|
+
except OSError as exc:
|
|
64
|
+
_fail(f"could not read {label} {path}: {exc}")
|
|
65
|
+
if not isinstance(data, dict):
|
|
66
|
+
_fail(f"{label} must contain a JSON object: {path}")
|
|
67
|
+
return data, raw
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _require_text(value: Any, field: str, *, max_len: int = 4096) -> str:
|
|
71
|
+
if not isinstance(value, str) or not value.strip():
|
|
72
|
+
_fail(f"{field} must be a non-empty string")
|
|
73
|
+
if len(value) > max_len or "\x00" in value:
|
|
74
|
+
_fail(f"{field} is not a bounded text value")
|
|
75
|
+
return value
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _require_text_list(value: Any, field: str, *, allow_empty: bool = False) -> list[str]:
|
|
79
|
+
if not isinstance(value, list) or (not value and not allow_empty):
|
|
80
|
+
_fail(f"{field} must be {'a list' if allow_empty else 'a non-empty list'}")
|
|
81
|
+
result: list[str] = []
|
|
82
|
+
for index, item in enumerate(value):
|
|
83
|
+
result.append(_require_text(item, f"{field}[{index}]"))
|
|
84
|
+
return result
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _require_id(value: Any, field: str) -> str:
|
|
88
|
+
text = _require_text(value, field, max_len=64)
|
|
89
|
+
if not ID_RE.fullmatch(text):
|
|
90
|
+
_fail(f"{field} must match {ID_RE.pattern}")
|
|
91
|
+
return text
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _reject_obvious_secrets(raw: bytes, label: str) -> None:
|
|
95
|
+
text = raw.decode("utf-8", errors="replace")
|
|
96
|
+
for pattern in SECRET_PATTERNS:
|
|
97
|
+
if pattern.search(text):
|
|
98
|
+
_fail(f"{label} appears to contain a credential; remove it before writing the artifact")
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _task_id(task_dir: Path) -> str:
|
|
102
|
+
task = load_task(task_dir)
|
|
103
|
+
if task is None:
|
|
104
|
+
_fail(f"task.json is missing or invalid: {task_dir}")
|
|
105
|
+
task_id = task.raw.get("id") or task.dir_name
|
|
106
|
+
if not isinstance(task_id, str) or not task_id.strip():
|
|
107
|
+
_fail(f"task has no usable id: {task_dir}")
|
|
108
|
+
if task.status not in {"planning", "in_progress"}:
|
|
109
|
+
_fail(f"task is not active (planning or in_progress): {task_dir}")
|
|
110
|
+
return task_id
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _task_lexical_from_resolved(task_resolved: Path, repo_root: Path) -> Path:
|
|
114
|
+
tasks_lexical = get_tasks_dir(repo_root)
|
|
115
|
+
try:
|
|
116
|
+
relative = task_resolved.relative_to(tasks_lexical.resolve())
|
|
117
|
+
except ValueError:
|
|
118
|
+
_fail(f"task is outside the Trellis task directory: {task_resolved}")
|
|
119
|
+
return tasks_lexical / relative
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def _assert_no_subpath_symlinks(task_dir: Path, target: Path) -> None:
|
|
123
|
+
"""Reject symlinks below a task while permitting a symlinked .trellis root."""
|
|
124
|
+
if task_dir.is_symlink():
|
|
125
|
+
_fail(f"refusing symlinked task directory: {task_dir}")
|
|
126
|
+
try:
|
|
127
|
+
parts = target.relative_to(task_dir).parts
|
|
128
|
+
except ValueError:
|
|
129
|
+
_fail(f"artifact path is outside its task: {target}")
|
|
130
|
+
current = task_dir
|
|
131
|
+
for part in parts:
|
|
132
|
+
current = current / part
|
|
133
|
+
if current.is_symlink():
|
|
134
|
+
_fail(f"refusing symlinked artifact path component: {current}")
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def _node_dir(task_dir: Path, work_id: str, subnode_id: str) -> Path:
|
|
138
|
+
return task_dir / "subnodes" / work_id / subnode_id
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def _relative_to_repo(path: Path, repo_root: Path) -> str:
|
|
142
|
+
try:
|
|
143
|
+
return path.relative_to(repo_root).as_posix()
|
|
144
|
+
except ValueError:
|
|
145
|
+
_fail(f"path is outside repository: {path}")
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def _validate_snapshot(value: Any) -> None:
|
|
149
|
+
if not isinstance(value, list) or not value:
|
|
150
|
+
_fail("source_snapshot must be a non-empty list")
|
|
151
|
+
for index, item in enumerate(value):
|
|
152
|
+
if not isinstance(item, dict):
|
|
153
|
+
_fail(f"source_snapshot[{index}] must be an object")
|
|
154
|
+
_require_text(item.get("locator"), f"source_snapshot[{index}].locator")
|
|
155
|
+
_require_text(
|
|
156
|
+
item.get("revision_or_digest"),
|
|
157
|
+
f"source_snapshot[{index}].revision_or_digest",
|
|
158
|
+
)
|
|
159
|
+
_require_text(item.get("observed_at"), f"source_snapshot[{index}].observed_at")
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def _validate_channel_ref(value: Any) -> None:
|
|
163
|
+
if not isinstance(value, dict):
|
|
164
|
+
_fail("channel_ref must be an object")
|
|
165
|
+
_require_text(value.get("name"), "channel_ref.name", max_len=128)
|
|
166
|
+
scope = _require_text(value.get("scope"), "channel_ref.scope", max_len=32)
|
|
167
|
+
if scope not in {"project", "global"}:
|
|
168
|
+
_fail("channel_ref.scope must be project or global")
|
|
169
|
+
_require_text(value.get("worker_handle"), "channel_ref.worker_handle", max_len=128)
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def _validate_brief_data(
|
|
173
|
+
brief: dict[str, Any],
|
|
174
|
+
task_dir: Path,
|
|
175
|
+
node_dir: Path,
|
|
176
|
+
repo_root: Path,
|
|
177
|
+
) -> None:
|
|
178
|
+
expected_task_id = _task_id(task_dir)
|
|
179
|
+
if brief.get("schema_version") != SCHEMA_VERSION:
|
|
180
|
+
_fail(f"brief.schema_version must be {SCHEMA_VERSION}")
|
|
181
|
+
if brief.get("task_id") != expected_task_id:
|
|
182
|
+
_fail("brief.task_id does not match task.json")
|
|
183
|
+
work_id = _require_id(brief.get("work_id"), "brief.work_id")
|
|
184
|
+
subnode_id = _require_id(brief.get("subnode_id"), "brief.subnode_id")
|
|
185
|
+
if node_dir != _node_dir(task_dir, work_id, subnode_id):
|
|
186
|
+
_fail("brief identity does not match its artifact directory")
|
|
187
|
+
if brief.get("role_id") != "subnode":
|
|
188
|
+
_fail("brief.role_id must be subnode")
|
|
189
|
+
_require_text(brief.get("question"), "brief.question")
|
|
190
|
+
_require_text(brief.get("independence_reason"), "brief.independence_reason")
|
|
191
|
+
_require_text_list(brief.get("scope"), "brief.scope")
|
|
192
|
+
_require_text_list(brief.get("protected_targets"), "brief.protected_targets", allow_empty=True)
|
|
193
|
+
_require_text(brief.get("lens"), "brief.lens")
|
|
194
|
+
_require_text(brief.get("evidence_method"), "brief.evidence_method")
|
|
195
|
+
_validate_snapshot(brief.get("source_snapshot"))
|
|
196
|
+
_require_text_list(brief.get("dependencies"), "brief.dependencies", allow_empty=True)
|
|
197
|
+
_require_text_list(brief.get("stop_conditions"), "brief.stop_conditions")
|
|
198
|
+
_require_text(brief.get("deadline"), "brief.deadline", max_len=128)
|
|
199
|
+
_validate_channel_ref(brief.get("channel_ref"))
|
|
200
|
+
if "retry_of" not in brief:
|
|
201
|
+
_fail("brief.retry_of must be present and null")
|
|
202
|
+
if brief["retry_of"] is not None:
|
|
203
|
+
_fail("brief.retry_of must be null; subnodes are not retry workers")
|
|
204
|
+
if "counter_of" not in brief:
|
|
205
|
+
_fail("brief.counter_of must be present; use null when this is not counterwork")
|
|
206
|
+
counter_of = brief["counter_of"]
|
|
207
|
+
if counter_of is not None:
|
|
208
|
+
counter_id = _require_id(counter_of, "brief.counter_of")
|
|
209
|
+
if counter_id == subnode_id:
|
|
210
|
+
_fail("brief.counter_of must name a different subnode")
|
|
211
|
+
expected_report = _relative_to_repo(node_dir / "report.json", repo_root)
|
|
212
|
+
if brief.get("report_path") != expected_report:
|
|
213
|
+
_fail("brief.report_path does not match its immutable artifact directory")
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def _resolve_artifact_file(
|
|
217
|
+
value: str,
|
|
218
|
+
expected_name: str,
|
|
219
|
+
repo_root: Path,
|
|
220
|
+
) -> tuple[Path, Path, Path]:
|
|
221
|
+
"""Return lexical artifact, node directory, and task directory after checks."""
|
|
222
|
+
supplied = Path(value)
|
|
223
|
+
lexical = supplied if supplied.is_absolute() else repo_root / supplied
|
|
224
|
+
if lexical.name != expected_name:
|
|
225
|
+
_fail(f"expected a {expected_name} path, received: {value}")
|
|
226
|
+
if lexical.is_symlink():
|
|
227
|
+
_fail(f"refusing symlinked artifact: {lexical}")
|
|
228
|
+
try:
|
|
229
|
+
resolved = lexical.resolve(strict=True)
|
|
230
|
+
except (OSError, RuntimeError) as exc:
|
|
231
|
+
_fail(f"could not resolve artifact {lexical}: {exc}")
|
|
232
|
+
node_resolved = resolved.parent
|
|
233
|
+
work_resolved = node_resolved.parent
|
|
234
|
+
subnodes_resolved = work_resolved.parent
|
|
235
|
+
task_resolved = subnodes_resolved.parent
|
|
236
|
+
if subnodes_resolved.name != "subnodes":
|
|
237
|
+
_fail(f"artifact is not under a task subnodes directory: {lexical}")
|
|
238
|
+
if not is_within_tasks_dir(task_resolved, repo_root):
|
|
239
|
+
_fail(f"artifact task is not a direct active task: {lexical}")
|
|
240
|
+
task_dir = _task_lexical_from_resolved(task_resolved, repo_root)
|
|
241
|
+
expected = _node_dir(task_dir, work_resolved.name, node_resolved.name) / expected_name
|
|
242
|
+
try:
|
|
243
|
+
same_path = lexical.resolve() == expected.resolve()
|
|
244
|
+
except (OSError, RuntimeError) as exc:
|
|
245
|
+
_fail(f"could not verify artifact containment: {exc}")
|
|
246
|
+
if not same_path:
|
|
247
|
+
_fail(f"artifact path does not match its task-owned location: {lexical}")
|
|
248
|
+
_assert_no_subpath_symlinks(task_dir, expected)
|
|
249
|
+
return expected, expected.parent, task_dir
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def _validate_brief_file(
|
|
253
|
+
value: str,
|
|
254
|
+
repo_root: Path,
|
|
255
|
+
) -> tuple[dict[str, Any], bytes, Path, Path, Path]:
|
|
256
|
+
brief_path, node_dir, task_dir = _resolve_artifact_file(value, "brief.json", repo_root)
|
|
257
|
+
brief, raw = _read_json_object(brief_path, MAX_DRAFT_BYTES, "brief")
|
|
258
|
+
_reject_obvious_secrets(raw, "brief")
|
|
259
|
+
_validate_brief_data(brief, task_dir, node_dir, repo_root)
|
|
260
|
+
return brief, raw, brief_path, node_dir, task_dir
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
def _validate_evidence(value: Any) -> set[tuple[str, str]]:
|
|
264
|
+
if not isinstance(value, list):
|
|
265
|
+
_fail("report.evidence must be a list")
|
|
266
|
+
identities: set[tuple[str, str]] = set()
|
|
267
|
+
for index, item in enumerate(value):
|
|
268
|
+
if not isinstance(item, dict):
|
|
269
|
+
_fail(f"report.evidence[{index}] must be an object")
|
|
270
|
+
evidence_id = _require_text(item.get("id"), f"report.evidence[{index}].id", max_len=128)
|
|
271
|
+
locator = _require_text(item.get("locator"), f"report.evidence[{index}].locator")
|
|
272
|
+
_require_text(item.get("summary"), f"report.evidence[{index}].summary")
|
|
273
|
+
identity = (evidence_id, locator)
|
|
274
|
+
if identity in identities:
|
|
275
|
+
_fail("report.evidence contains a duplicate evidence identity")
|
|
276
|
+
identities.add(identity)
|
|
277
|
+
return identities
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
def _validate_report_data(
|
|
281
|
+
report: dict[str, Any],
|
|
282
|
+
brief: dict[str, Any],
|
|
283
|
+
brief_raw: bytes,
|
|
284
|
+
) -> set[tuple[str, str]]:
|
|
285
|
+
if report.get("schema_version") != SCHEMA_VERSION:
|
|
286
|
+
_fail(f"report.schema_version must be {SCHEMA_VERSION}")
|
|
287
|
+
for field in ("task_id", "work_id", "subnode_id", "role_id"):
|
|
288
|
+
if report.get(field) != brief.get(field):
|
|
289
|
+
_fail(f"report.{field} does not match brief.{field}")
|
|
290
|
+
digest = hashlib.sha256(brief_raw).hexdigest()
|
|
291
|
+
if report.get("brief_digest") != digest:
|
|
292
|
+
_fail("report.brief_digest does not match immutable brief.json")
|
|
293
|
+
if report.get("scope") != brief.get("scope"):
|
|
294
|
+
_fail("report.scope does not match brief.scope")
|
|
295
|
+
if report.get("lens") != brief.get("lens"):
|
|
296
|
+
_fail("report.lens does not match brief.lens")
|
|
297
|
+
status = report.get("status")
|
|
298
|
+
if status not in {"complete", "blocked", "incomplete", "error"}:
|
|
299
|
+
_fail("report.status must be complete, blocked, incomplete, or error")
|
|
300
|
+
evidence = _validate_evidence(report.get("evidence"))
|
|
301
|
+
for field in ("findings", "uncertainties", "corrections"):
|
|
302
|
+
if not isinstance(report.get(field), list):
|
|
303
|
+
_fail(f"report.{field} must be a list")
|
|
304
|
+
if status == "complete" and not evidence:
|
|
305
|
+
_fail("a complete report requires at least one evidence item")
|
|
306
|
+
if status != "complete":
|
|
307
|
+
_require_text_list(report.get("completed_scope"), "report.completed_scope")
|
|
308
|
+
_require_text(report.get("blocker"), "report.blocker")
|
|
309
|
+
return evidence
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
def _validate_report_file(
|
|
313
|
+
value: str,
|
|
314
|
+
repo_root: Path,
|
|
315
|
+
) -> tuple[dict[str, Any], set[tuple[str, str]], dict[str, Any], Path]:
|
|
316
|
+
report_path, node_dir, _task_dir = _resolve_artifact_file(value, "report.json", repo_root)
|
|
317
|
+
brief_path = node_dir / "brief.json"
|
|
318
|
+
brief, brief_raw, _brief_path, _brief_node, _brief_task = _validate_brief_file(
|
|
319
|
+
str(brief_path), repo_root
|
|
320
|
+
)
|
|
321
|
+
expected_report = _relative_to_repo(report_path, repo_root)
|
|
322
|
+
if brief.get("report_path") != expected_report:
|
|
323
|
+
_fail("brief.report_path does not point to the report being validated")
|
|
324
|
+
report, raw = _read_json_object(report_path, MAX_REPORT_BYTES, "report")
|
|
325
|
+
_reject_obvious_secrets(raw, "report")
|
|
326
|
+
evidence = _validate_report_data(report, brief, brief_raw)
|
|
327
|
+
return report, evidence, brief, node_dir
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
def _init(args: argparse.Namespace) -> None:
|
|
331
|
+
repo_root = get_repo_root()
|
|
332
|
+
task_dir = resolve_task_dir(args.task, repo_root)
|
|
333
|
+
if task_dir is None or not is_within_tasks_dir(task_dir, repo_root):
|
|
334
|
+
_fail("--task must identify a direct active task directory")
|
|
335
|
+
if task_dir.is_symlink():
|
|
336
|
+
_fail(f"refusing symlinked task directory: {task_dir}")
|
|
337
|
+
work_id = _require_id(args.work_id, "--work-id")
|
|
338
|
+
subnode_id = _require_id(args.subnode_id, "--subnode-id")
|
|
339
|
+
node_dir = _node_dir(task_dir, work_id, subnode_id)
|
|
340
|
+
_assert_no_subpath_symlinks(task_dir, node_dir)
|
|
341
|
+
if node_dir.exists() or node_dir.is_symlink():
|
|
342
|
+
_fail(f"subnode artifacts already exist: {node_dir}")
|
|
343
|
+
draft_path = Path(args.draft)
|
|
344
|
+
if not draft_path.is_absolute():
|
|
345
|
+
draft_path = repo_root / draft_path
|
|
346
|
+
draft, raw = _read_json_object(draft_path, MAX_DRAFT_BYTES, "brief draft")
|
|
347
|
+
_reject_obvious_secrets(raw, "brief draft")
|
|
348
|
+
expected_task_id = _task_id(task_dir)
|
|
349
|
+
brief = dict(draft)
|
|
350
|
+
brief["schema_version"] = SCHEMA_VERSION
|
|
351
|
+
brief["task_id"] = expected_task_id
|
|
352
|
+
brief["work_id"] = work_id
|
|
353
|
+
brief["subnode_id"] = subnode_id
|
|
354
|
+
brief["role_id"] = "subnode"
|
|
355
|
+
brief["report_path"] = _relative_to_repo(node_dir / "report.json", repo_root)
|
|
356
|
+
_validate_brief_data(brief, task_dir, node_dir, repo_root)
|
|
357
|
+
for directory in (task_dir / "subnodes", task_dir / "subnodes" / work_id, node_dir):
|
|
358
|
+
if directory.is_symlink():
|
|
359
|
+
_fail(f"refusing symlinked artifact directory: {directory}")
|
|
360
|
+
if directory.exists() and not directory.is_dir():
|
|
361
|
+
_fail(f"artifact path component is not a directory: {directory}")
|
|
362
|
+
try:
|
|
363
|
+
directory.mkdir(exist_ok=True)
|
|
364
|
+
except OSError as exc:
|
|
365
|
+
_fail(f"could not create artifact directory {directory}: {exc}")
|
|
366
|
+
brief_path = node_dir / "brief.json"
|
|
367
|
+
worklog_path = node_dir / "worklog.md"
|
|
368
|
+
brief_text = json.dumps(brief, indent=2, ensure_ascii=False) + "\n"
|
|
369
|
+
worklog_text = (
|
|
370
|
+
"# Subnode Worklog\n\n"
|
|
371
|
+
f"- Task: `{brief['task_id']}`\n"
|
|
372
|
+
f"- Work: `{work_id}`\n"
|
|
373
|
+
f"- Subnode: `{subnode_id}`\n\n"
|
|
374
|
+
"Append substantive observations, corrections, blockers, and completion notes below.\n"
|
|
375
|
+
)
|
|
376
|
+
if not write_text_atomic(brief_path, brief_text):
|
|
377
|
+
_fail(f"could not write immutable brief: {brief_path}")
|
|
378
|
+
if not write_text_atomic(worklog_path, worklog_text):
|
|
379
|
+
_fail(f"could not write subnode worklog: {worklog_path}")
|
|
380
|
+
print(json.dumps({
|
|
381
|
+
"brief": _relative_to_repo(brief_path, repo_root),
|
|
382
|
+
"worklog": _relative_to_repo(worklog_path, repo_root),
|
|
383
|
+
"report": brief["report_path"],
|
|
384
|
+
}))
|
|
385
|
+
|
|
386
|
+
|
|
387
|
+
def _validate(args: argparse.Namespace) -> None:
|
|
388
|
+
repo_root = get_repo_root()
|
|
389
|
+
report, _evidence, _brief, _node_dir_value = _validate_report_file(args.report, repo_root)
|
|
390
|
+
print(f"Validated pending-review report: {report['subnode_id']}")
|
|
391
|
+
|
|
392
|
+
|
|
393
|
+
def _validate_counter(args: argparse.Namespace) -> None:
|
|
394
|
+
repo_root = get_repo_root()
|
|
395
|
+
primary_dir = Path(args.primary)
|
|
396
|
+
counter_dir = Path(args.counter)
|
|
397
|
+
if not primary_dir.is_absolute():
|
|
398
|
+
primary_dir = repo_root / primary_dir
|
|
399
|
+
if not counter_dir.is_absolute():
|
|
400
|
+
counter_dir = repo_root / counter_dir
|
|
401
|
+
primary_report, primary_evidence, primary_brief, primary_node = _validate_report_file(
|
|
402
|
+
str(primary_dir / "report.json"), repo_root
|
|
403
|
+
)
|
|
404
|
+
counter_report, counter_evidence, counter_brief, counter_node = _validate_report_file(
|
|
405
|
+
str(counter_dir / "report.json"), repo_root
|
|
406
|
+
)
|
|
407
|
+
if primary_node == counter_node:
|
|
408
|
+
_fail("counter validation requires two different subnode directories")
|
|
409
|
+
if primary_brief["task_id"] != counter_brief["task_id"]:
|
|
410
|
+
_fail("counter subnode must belong to the same task")
|
|
411
|
+
if primary_brief["work_id"] != counter_brief["work_id"]:
|
|
412
|
+
_fail("counter subnode must belong to the same work_id")
|
|
413
|
+
if counter_brief.get("counter_of") != primary_brief["subnode_id"]:
|
|
414
|
+
_fail("counter brief.counter_of must identify the primary subnode")
|
|
415
|
+
if primary_brief["lens"] == counter_brief["lens"]:
|
|
416
|
+
_fail("counter subnode must use a different lens")
|
|
417
|
+
if primary_report["status"] != "complete" or counter_report["status"] != "complete":
|
|
418
|
+
_fail("counter comparison requires two complete reports")
|
|
419
|
+
if primary_evidence & counter_evidence:
|
|
420
|
+
_fail("counter report must use evidence identities independent of the primary report")
|
|
421
|
+
print(
|
|
422
|
+
"Validated independent counter reports: "
|
|
423
|
+
f"{primary_brief['subnode_id']} and {counter_brief['subnode_id']}"
|
|
424
|
+
)
|
|
425
|
+
|
|
426
|
+
|
|
427
|
+
def _parser() -> argparse.ArgumentParser:
|
|
428
|
+
parser = argparse.ArgumentParser(description=__doc__)
|
|
429
|
+
subparsers = parser.add_subparsers(dest="command", required=True)
|
|
430
|
+
|
|
431
|
+
init = subparsers.add_parser("init", help="create immutable brief and empty worklog")
|
|
432
|
+
init.add_argument("--task", required=True, help="direct active Trellis task directory")
|
|
433
|
+
init.add_argument("--work-id", required=True)
|
|
434
|
+
init.add_argument("--subnode-id", required=True)
|
|
435
|
+
init.add_argument("--draft", required=True, help="coordinator-owned brief draft JSON")
|
|
436
|
+
init.set_defaults(handler=_init)
|
|
437
|
+
|
|
438
|
+
validate = subparsers.add_parser("validate", help="validate a pending-review report")
|
|
439
|
+
validate.add_argument("--report", required=True)
|
|
440
|
+
validate.set_defaults(handler=_validate)
|
|
441
|
+
|
|
442
|
+
counter = subparsers.add_parser(
|
|
443
|
+
"validate-counter",
|
|
444
|
+
help="validate two completed, independent subnode directories",
|
|
445
|
+
)
|
|
446
|
+
counter.add_argument("--primary", required=True, help="primary subnode artifact directory")
|
|
447
|
+
counter.add_argument("--counter", required=True, help="counter subnode artifact directory")
|
|
448
|
+
counter.set_defaults(handler=_validate_counter)
|
|
449
|
+
return parser
|
|
450
|
+
|
|
451
|
+
|
|
452
|
+
def main() -> int:
|
|
453
|
+
args = _parser().parse_args()
|
|
454
|
+
try:
|
|
455
|
+
args.handler(args)
|
|
456
|
+
except ArtifactError as exc:
|
|
457
|
+
print(f"Error: {exc}", file=sys.stderr)
|
|
458
|
+
return 1
|
|
459
|
+
return 0
|
|
460
|
+
|
|
461
|
+
|
|
462
|
+
if __name__ == "__main__":
|
|
463
|
+
sys.exit(main())
|