@hybridlabor-api/aos 4.7.0 → 4.7.1

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 (34) hide show
  1. package/.agents/agents.md +8 -3
  2. package/.agents/nodes.json +2 -1
  3. package/.agents/state.schema.json +24 -0
  4. package/.agents/vendor-manifest.json +6 -0
  5. package/.agents/workflows/startcycle-graph.md +2 -2
  6. package/.claude/agents/architect.md +5 -4
  7. package/.claude/agents/techlead.md +2 -0
  8. package/.claude/workflows/startcycle-dispatch.mjs +175 -17
  9. package/.opencode/agents/architect.md +5 -4
  10. package/.opencode/agents/techlead.md +2 -0
  11. package/THIRD_PARTY_NOTICES.md +9 -1
  12. package/bin/aos-doctor.mjs +366 -0
  13. package/bin/aos-store.mjs +167 -0
  14. package/docs/AUDIT_BRIEF_ECC_STORE.md +122 -0
  15. package/docs/PLAN_AOS_STORE_PHASE1.md +190 -0
  16. package/installer.js +41 -1
  17. package/lib/aos-archify-contract.mjs +277 -0
  18. package/lib/ecc-store-index.json +1 -0
  19. package/mcp_config.json +4 -0
  20. package/mcps/mcsc/packages/core/src/adapters/codex.js +160 -37
  21. package/package.json +6 -2
  22. package/scripts/build-ecc-store-index.mjs +147 -0
  23. package/skills/basic/startcycle-graph/SKILL.md +13 -0
  24. package/skills/global_config/agenttrail/bin/agenttrail.mjs +153 -42
  25. package/skills/global_config/agenttrail/public/index.html +5 -4
  26. package/skills/global_config/archify/examples/dataflow-product-analytics.html +15048 -0
  27. package/skills/global_config/archify/examples/lifecycle-agent-run.html +14983 -0
  28. package/skills/global_config/archify/examples/sequence-cache-miss-request.html +15063 -0
  29. package/skills/global_config/archify/examples/web-app-rendered.html +15012 -0
  30. package/skills/global_config/archify/examples/workflow-agent-tool-call-rendered.html +15054 -0
  31. package/skills/global_config/ask-tim/SKILL.md +10 -0
  32. package/skills/global_config/mcsc/SKILL.md +60 -0
  33. package/skills/global_config/plan-canvas/scripts/lib/plan-canvas/server.js +31 -6
  34. package/skills/global_config/plan-canvas/scripts/plan-canvas.js +4 -2
@@ -0,0 +1,190 @@
1
+ # Execution Plan: AOS Skill, Subagent & Extras Store (Phase 1)
2
+
3
+ **Target Repository:** `/Users/timrennings/dev/bdb-dev/bdb-dev-optimized-agent-skills`
4
+ **Target Worktree:** `/Users/timrennings/dev/bdb-dev/aos-wt-store`
5
+ **Branch:** `feat/aos-store` (branched from `main`)
6
+ **Scope:** Phase 1 — Core Store Infrastructure, Offline Index, CLI, and Dispatcher Fallback
7
+ **Audience:** Autonomous Implementation Agent
8
+ **Upstream Source:** `https://github.com/affaan-m/ECC` (Pinned Commit, MIT License)
9
+
10
+ ---
11
+
12
+ ## 🎯 1. Mission & Architectural Invariants
13
+
14
+ You are implementing Phase 1 of the **AOS Store** (`@hybridlabor-api/aos`). This provides an on-demand fallback layer for skills and subagents sourced from `affaan-m/ECC` whenever a requested capability is not in AOS's 185-skill core.
15
+
16
+ ### ⚠️ Non-Negotiable Invariants (Audit-Mandated)
17
+ 1. **No Forks:** Do NOT fork ECC to GitHub. Pin `affaan-m/ECC` directly via Git SHA in `vendor-manifest.json` (identical to `anti-slop` and `skylos`).
18
+ 2. **No Mid-Run Downloads:** Never download or prompt to download during a `/startcycle-graph` run (violates the GO-Gate and headless guarantees). If `--skill=<name>` is missing, the dispatcher must escalate with a clear manual install instruction:
19
+ ```text
20
+ "<name> is not installed. Found in the ECC store.
21
+ Run: aos store install <name>
22
+ Then re-run your startcycle command."
23
+ ```
24
+ 3. **No File System in Dispatcher:** `.claude/workflows/startcycle-dispatch.mjs` has no direct `fs` access. The mandatory skill check is an `agent()` call (Haiku); you must extend its JSON schema with `store_matches: string[]` instead of adding `fs` calls.
25
+ 4. **Trust Anchor is AOS Package:** `lib/ecc-store-index.json` ships inside the `@hybridlabor-api/aos` npm package. Downloads from GitHub must match the SHA-256 hash in this index. Hash mismatch = hard failure.
26
+ 5. **Phase-1 Scope: Markdown Only:**
27
+ - Only import `skills/` and `agents/` (Subagents).
28
+ - **Strictly exclude** `scripts/`, `hooks/`, and `rules/` (prevents RCE and config pollution).
29
+ 6. **Collision Invariant:** AOS core skills ALWAYS take precedence. If an ECC skill shares a name with an existing AOS skill, it is skipped and logged.
30
+ 7. **Future-Proofing for Fucksaas & AO:** Every store item must contain schema fields for `tier: "free"`, `requires_auth: false`, and `origin: "ecc"` so Fucksaas PRO pay-gates and AO marketplace UI can be layered on in Phase 2 without changing the schema.
31
+
32
+ ---
33
+
34
+ ## 🛠️ 2. Worktree Initialization
35
+
36
+ Execute the following commands to create and enter the isolated worktree:
37
+
38
+ ```bash
39
+ cd /Users/timrennings/dev/bdb-dev/bdb-dev-optimized-agent-skills
40
+ git fetch origin main
41
+ git worktree add -b feat/aos-store /Users/timrennings/dev/bdb-dev/aos-wt-store main
42
+ cd /Users/timrennings/dev/bdb-dev/aos-wt-store
43
+ ```
44
+
45
+ All modifications below MUST take place inside `/Users/timrennings/dev/bdb-dev/aos-wt-store/`.
46
+
47
+ ---
48
+
49
+ ## 📋 3. Step-by-Step Implementation Checklist
50
+
51
+ ### Step 1: Upstream Pinning & Provenance
52
+ - [ ] Inspect the latest stable commit SHA of `affaan-m/ECC`:
53
+ ```bash
54
+ gh api repos/affaan-m/ECC/commits/main --jq '.sha'
55
+ ```
56
+ - [ ] Add `ecc-store` entry to `vendor-manifest.json`:
57
+ ```json
58
+ "ecc-store": {
59
+ "upstream": "https://github.com/affaan-m/ECC",
60
+ "pinned_commit": "<FULL_SHA>",
61
+ "purpose": "On-demand fallback store for skills and auxiliary subagents",
62
+ "target": "lib/ecc-store-index.json"
63
+ }
64
+ ```
65
+ - [ ] Update `THIRD_PARTY_NOTICES.md` to document the full scope of ECC components and MIT license terms.
66
+
67
+ ### Step 2: Store Index Generator (`scripts/build-ecc-store-index.mjs`)
68
+ - [ ] Create `scripts/build-ecc-store-index.mjs`. The script must:
69
+ 1. Fetch the tree from GitHub API for the pinned commit (`repos/affaan-m/ECC/git/trees/<sha>?recursive=1`) or clone to a temporary cache.
70
+ 2. Filter strictly for Markdown:
71
+ - `skills/**/SKILL.md` or `skills/**/*.md`
72
+ - `agents/*.md`
73
+ 3. Validate & sanitize frontmatter:
74
+ - Must conform to AOS standards (runnable through `scripts/validate-skills.mjs` rules).
75
+ - Compute SHA-256 for each raw file.
76
+ - Scan for forbidden tokens (hardcoded Claude-only tools like `Bash`, `View`; replace or flag compatibility).
77
+ 4. Build schema:
78
+ ```typescript
79
+ interface StoreIndex {
80
+ version: "1.0.0";
81
+ generated_at: string;
82
+ pinned_commit: string;
83
+ skills: Record<string, {
84
+ name: string;
85
+ description: string;
86
+ category: string;
87
+ tier: "free";
88
+ origin: "ecc";
89
+ requires_auth: false;
90
+ sha256: string;
91
+ upstream_path: string;
92
+ harness_support: ("claude" | "antigravity" | "codex" | "cursor" | "roo")[];
93
+ }>;
94
+ subagents: Record<string, {
95
+ name: string;
96
+ description: string;
97
+ role: string;
98
+ tier: "free";
99
+ origin: "ecc";
100
+ sha256: string;
101
+ upstream_path: string;
102
+ }>;
103
+ }
104
+ ```
105
+ 5. Check collisions against local `skills/` in AOS. Any collision is skipped.
106
+ 6. Output the compiled index to `lib/ecc-store-index.json` (keep size under 100 KB).
107
+ - [ ] Run `node scripts/build-ecc-store-index.mjs` and verify `lib/ecc-store-index.json`.
108
+
109
+ ### Step 3: CLI Implementation (`bin/aos-store.mjs` & `installer.js`)
110
+ - [ ] Create `bin/aos-store.mjs`:
111
+ - `aos store list [--type=skills|agents]` -> Formatted table of available items.
112
+ - `aos store search <query>` -> Search by name, tags, description in `lib/ecc-store-index.json`.
113
+ - `aos store install <name> [--project]` ->
114
+ 1. Look up `<name>` in `lib/ecc-store-index.json`.
115
+ 2. Check if already installed locally; collision guard against AOS core.
116
+ 3. Download raw content from `https://raw.githubusercontent.com/affaan-m/ECC/<pinned_sha>/<upstream_path>`.
117
+ 4. Compute SHA-256 and assert match with index (halt on mismatch).
118
+ 5. Write file:
119
+ - If `--project`: write to `./skills/<name>/SKILL.md`.
120
+ - If global: fan-out to all active harnesses (`~/.agents/skills/<name>/SKILL.md`, `~/.claude/skills/<name>/SKILL.md`, etc., reusing logic from `installer.js`).
121
+ 6. Log success with green checkmark.
122
+ - [ ] Make `bin/aos-store.mjs` executable (`chmod +x bin/aos-store.mjs`).
123
+ - [ ] Register in `package.json`:
124
+ ```json
125
+ "bin": {
126
+ ...
127
+ "aos-store": "bin/aos-store.mjs"
128
+ }
129
+ ```
130
+ - [ ] Wire up `aos store <args>` in `installer.js` so running `aos store search` forwards directly to `bin/aos-store.mjs`.
131
+
132
+ ### Step 4: Dispatcher Hook (`.claude/workflows/startcycle-dispatch.mjs`)
133
+ - [ ] Locate the `validate-mandatory-skills` agent call around lines 701–741.
134
+ - [ ] Update the prompt and JSON schema:
135
+ - Add `store_matches: { type: 'array', items: { type: 'string' } }` to the output schema.
136
+ - Instruct the agent to check `lib/ecc-store-index.json` for any skill not found locally on disk.
137
+ - [ ] Update escalation handling (lines 728–739):
138
+ ```javascript
139
+ const missing = skillCheckResult?.missing ?? [];
140
+ if (missing.length > 0) {
141
+ const storeMatches = skillCheckResult?.store_matches ?? [];
142
+ const suggestions = skillCheckResult?.suggestions ?? [];
143
+
144
+ let msg = `--skill named skill(s) that are not currently installed: ${missing.join(', ')}.\n`;
145
+ if (storeMatches.length > 0) {
146
+ msg += `Found in the AOS / ECC Store: ${storeMatches.join(', ')}.\n` +
147
+ `Run: aos store install ${storeMatches.join(' ')}\n` +
148
+ `Then re-run your startcycle command.\n`;
149
+ } else if (suggestions.length > 0) {
150
+ msg += `Did you mean: ${suggestions.join(', ')}?\n`;
151
+ }
152
+ return await escalate(msg);
153
+ }
154
+ ```
155
+
156
+ ### Step 5: Router Update (`skills/global_config/ask-tim/SKILL.md`)
157
+ - [ ] Add a new section in `ask-tim/SKILL.md` titled `## Outside the core: the ECC store`:
158
+ - Explain that when no native AOS skill fits (e.g. Django, Spring, Godot), the user can query the store via `aos store search <query>`.
159
+ - Document `aos store install <name>` for on-demand activation.
160
+ - Reiterate that the core remains 185 high-trust skills.
161
+
162
+ ### Step 6: Verification & Quality Gate
163
+ - [ ] Run skill validator:
164
+ ```bash
165
+ npm run test
166
+ npm run validate:strict
167
+ ```
168
+ - [ ] Test the CLI commands:
169
+ ```bash
170
+ node bin/aos-store.mjs list | head -n 20
171
+ node bin/aos-store.mjs search django
172
+ node bin/aos-store.mjs search spring
173
+ ```
174
+ - [ ] Test mock installation:
175
+ ```bash
176
+ node bin/aos-store.mjs install <test-skill> --dry-run
177
+ ```
178
+ - [ ] Ensure Git working tree is clean and commits follow Conventional Commits:
179
+ - `feat(store): add build-ecc-store-index generator and store-index.json`
180
+ - `feat(store): implement aos-store CLI and installer integration`
181
+ - `feat(dispatcher): suggest aos store install on missing mandatory skills`
182
+ - `docs(ask-tim): add ECC store fallback section`
183
+
184
+ ---
185
+
186
+ ## 🚀 4. Definition of Done
187
+ 1. `npm test` and `npm run validate` pass with zero errors.
188
+ 2. `lib/ecc-store-index.json` is generated, valid JSON, under 100 KB.
189
+ 3. `bin/aos-store.mjs` executes cleanly without unhandled rejections.
190
+ 4. Worktree is ready for review and merge into `main`.
package/installer.js CHANGED
@@ -3608,7 +3608,7 @@ function installGlobalBinaries() {
3608
3608
  }
3609
3609
 
3610
3610
  const isWin = process.platform === 'win32';
3611
- const cliBins = ['aos-config', 'aos-dashboard', 'aos-uninstall'];
3611
+ const cliBins = ['aos-config', 'aos-dashboard', 'aos-uninstall', 'aos-store', 'aos-doctor'];
3612
3612
 
3613
3613
  for (const name of cliBins) {
3614
3614
  const targetMjs = path.join(globalAgentsBin, `${name}.mjs`);
@@ -4809,7 +4809,38 @@ async function runQuickUpdate(installState) {
4809
4809
  }
4810
4810
 
4811
4811
 
4812
+ function buildAoAnnouncementBanner() {
4813
+ return [
4814
+ '\x1b[36m╭──────────────────────────────────────────────────────────────────────────────╮\x1b[0m',
4815
+ '\x1b[36m│\x1b[0m \x1b[36m│\x1b[0m',
4816
+ '\x1b[36m│\x1b[0m \x1b[1m\x1b[35m🚀 BDB AGENT ORCHESTRATOR APP — FINALE BETA JETZT VERFÜGBAR!\x1b[0m \x1b[36m│\x1b[0m',
4817
+ '\x1b[36m│\x1b[0m \x1b[36m│\x1b[0m',
4818
+ '\x1b[36m│\x1b[0m Die nächste Generation der Cross-Harness Multi-Agenten-Orchestrierung \x1b[36m│\x1b[0m',
4819
+ '\x1b[36m│\x1b[0m ist jetzt als finale Beta für alle User freigeschaltet. \x1b[36m│\x1b[0m',
4820
+ '\x1b[36m│\x1b[0m \x1b[36m│\x1b[0m',
4821
+ '\x1b[36m│\x1b[0m • \x1b[1mDashboard & WebUI:\x1b[0m http://localhost:3101 \x1b[36m│\x1b[0m',
4822
+ '\x1b[36m│\x1b[0m • \x1b[1mService-Befehl:\x1b[0m ao service install (Hintergrunddienst aktivieren) \x1b[36m│\x1b[0m',
4823
+ '\x1b[36m│\x1b[0m • \x1b[1mQuick Launch:\x1b[0m ao open oder ao service status \x1b[36m│\x1b[0m',
4824
+ '\x1b[36m│\x1b[0m • \x1b[1mFeatures:\x1b[0m Session-Telemetrie, Live-AgentTrail & Multi-Workspaces\x1b[36m│\x1b[0m',
4825
+ '\x1b[36m│\x1b[0m \x1b[36m│\x1b[0m',
4826
+ '\x1b[36m╰──────────────────────────────────────────────────────────────────────────────╯\x1b[0m\n'
4827
+ ].join('\n');
4828
+ }
4829
+
4812
4830
  async function main() {
4831
+ if (process.argv[2] === 'store') {
4832
+ const storeScript = path.join(srcDir, 'bin', 'aos-store.mjs');
4833
+ const result = spawnSync(process.execPath, [storeScript, ...process.argv.slice(3)], { stdio: 'inherit' });
4834
+ process.exitCode = result.status == null ? 1 : result.status;
4835
+ return;
4836
+ }
4837
+ if (process.argv[2] === 'doctor' || process.argv[2] === 'checkup' || process.argv.includes('--doctor')) {
4838
+ const doctorScript = path.join(srcDir, 'bin', 'aos-doctor.mjs');
4839
+ const doctorArgs = process.argv.slice(process.argv[2] === 'doctor' || process.argv[2] === 'checkup' ? 3 : 2).filter(a => a !== '--doctor');
4840
+ const result = spawnSync(process.execPath, [doctorScript, ...doctorArgs], { stdio: 'inherit' });
4841
+ process.exitCode = result.status == null ? 1 : result.status;
4842
+ return;
4843
+ }
4813
4844
  const skipIntro = isAutoYes || process.argv.includes('--no-intro') || process.argv.includes('--no-animation');
4814
4845
  await renderKineticIntro({ rotations: 1, fps: 12.5, skip: skipIntro, pkgVersion: pkg.version });
4815
4846
  console.log(buildWordmarkBanner());
@@ -4827,6 +4858,7 @@ async function main() {
4827
4858
 
4828
4859
  intro(buildHeroHeader(pkg.version));
4829
4860
  console.log(buildTelemetryCard({ installState, detections, daemonStatus }));
4861
+ console.log(buildAoAnnouncementBanner());
4830
4862
 
4831
4863
  if (DRY_RUN) {
4832
4864
  // Precise on purpose: version comparison still queries the registry, and
@@ -4869,12 +4901,14 @@ async function main() {
4869
4901
  const options = installState.updateAvailable
4870
4902
  ? [
4871
4903
  { value: 'quick', label: `⚡ Quick Update (v${installState.localVersion} ➔ v${installState.currentVersion})`, hint: 'refresh skills, templates & daemons' },
4904
+ { value: 'doctor', label: '🩺 Run System Checkup / Doctor', hint: 'verify environment, placement, daemons & AO' },
4872
4905
  { value: 'project', label: '📁 Drop Local Project Harness', hint: 'copy dispatcher contract to cwd' },
4873
4906
  { value: 'reconfigure', label: '🛠️ Reconfigure System', hint: 'change targets, tier or options' },
4874
4907
  { value: 'uninstall', label: '🗑️ Uninstall AOS', hint: 'remove what this installer placed; your data stays' },
4875
4908
  { value: 'cancel', label: '❌ Exit' }
4876
4909
  ]
4877
4910
  : [
4911
+ { value: 'doctor', label: '🩺 Run System Checkup / Doctor', hint: 'verify environment, placement, daemons & AO' },
4878
4912
  { value: 'project', label: '📁 Drop Local Project Harness', hint: 'copy dispatcher contract to cwd' },
4879
4913
  { value: 'quick', label: '🔄 Verify & Refresh All Skills', hint: 're-sync and health check' },
4880
4914
  { value: 'reconfigure', label: '🛠️ Reconfigure System', hint: 'switch tier or targets' },
@@ -4892,6 +4926,12 @@ async function main() {
4892
4926
  outro('Cancelled.');
4893
4927
  return;
4894
4928
  }
4929
+ if (action === 'doctor') {
4930
+ const doctorScript = path.join(srcDir, 'bin', 'aos-doctor.mjs');
4931
+ spawnSync(process.execPath, [doctorScript], { stdio: 'inherit' });
4932
+ outro('System Checkup complete.');
4933
+ return;
4934
+ }
4895
4935
  if (action === 'project') {
4896
4936
  installProjectHarness();
4897
4937
  outro('Project harness installation complete.');
@@ -0,0 +1,277 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+
4
+ export const ARCHITECTURE_SPEC_PATH = 'production_artifacts/00_architecture.json';
5
+ export const ARCHITECTURE_HTML_PATH = 'production_artifacts/00_architecture.html';
6
+ export const ARCHITECTURE_STATE_KEY = 'architecture';
7
+ export const ARCHITECTURE_CHECK_COUNT = 9;
8
+ export const RECEIPT_MAX_BYTES = 65536;
9
+ export const LOG_MAX_CHARS = 400;
10
+ export const ARCHIFY_REL_BIN = 'skills/global_config/archify/bin/archify.mjs';
11
+ export const ARCHIFY_TIMEOUT_MS = 120000;
12
+ export const ARCHIFY_MAX_BYTES = 65536;
13
+ export const PLAN_CANVAS_BIN = 'aos-plan-canvas';
14
+ export const PLAN_CANVAS_TIMEOUT_MS = 15000;
15
+ export const PLAN_CANVAS_MAX_BYTES = 16384;
16
+ export const AGENT_TRAIL_BIN = 'aos-trail';
17
+ export const EXECUTION_PLAN_PATH = 'production_artifacts/00_execution_plan.md';
18
+
19
+ const ALLOWED_PATHS = new Set([ARCHITECTURE_SPEC_PATH, ARCHITECTURE_HTML_PATH]);
20
+ const SHA256_RE = /^[a-fA-F0-9]{64}$/;
21
+ const LONG_HEX_RE = /[a-fA-F0-9]{64}/g;
22
+ const HOME_RE = /\/(Users|home)\/[^\s/]+/g;
23
+
24
+ export function isAllowedArchitecturePath(value) {
25
+ if (typeof value !== 'string') return false;
26
+ return ALLOWED_PATHS.has(value);
27
+ }
28
+
29
+ function boundedString(raw, maxBytes) {
30
+ if (typeof raw !== 'string') return { ok: false, value: '' };
31
+ if (Buffer.byteLength(raw, 'utf8') > maxBytes) return { ok: false, value: '' };
32
+ return { ok: true, value: raw };
33
+ }
34
+
35
+ function firstDiagnosticMessage(diagnostics) {
36
+ if (!Array.isArray(diagnostics)) return '';
37
+ for (const entry of diagnostics) {
38
+ if (entry && typeof entry.message === 'string' && entry.message) return entry.message;
39
+ if (entry && typeof entry.code === 'string' && entry.code) return entry.code;
40
+ }
41
+ return '';
42
+ }
43
+
44
+ export function parseDeliverReceipt(raw, options = {}) {
45
+ const maxBytes = Number.isInteger(options.maxBytes) ? options.maxBytes : RECEIPT_MAX_BYTES;
46
+ const bounded = boundedString(raw, maxBytes);
47
+ if (!bounded.ok) {
48
+ return { ok: false, error: 'receipt exceeds bounded size, refusing to parse' };
49
+ }
50
+ let obj = null;
51
+ try {
52
+ obj = JSON.parse(bounded.value);
53
+ } catch {
54
+ return { ok: false, error: 'receipt is not valid JSON' };
55
+ }
56
+ if (!obj || typeof obj !== 'object') {
57
+ return { ok: false, error: 'receipt is not a JSON object' };
58
+ }
59
+ if (obj.ok !== true) {
60
+ const detail = (typeof obj.error === 'string' && obj.error) || firstDiagnosticMessage(obj.diagnostics) || 'archify deliver failed';
61
+ return { ok: false, error: String(detail).slice(0, 300), stage: typeof obj.stage === 'string' ? obj.stage : 'unknown' };
62
+ }
63
+ const validation = obj.validation && typeof obj.validation === 'object' ? obj.validation : null;
64
+ const checksPassed = validation ? validation.checksPassed : undefined;
65
+ const checkCount = validation ? validation.checkCount : undefined;
66
+ const errors = validation ? validation.errors : undefined;
67
+ const artifactSha = obj.artifact && typeof obj.artifact.sha256 === 'string' ? obj.artifact.sha256 : '';
68
+ const specSha = obj.specification && typeof obj.specification.sha256 === 'string' ? obj.specification.sha256 : '';
69
+ if (checksPassed !== checkCount || checkCount !== ARCHITECTURE_CHECK_COUNT || errors !== 0) {
70
+ return { ok: false, error: `deliver receipt incomplete: ${checksPassed}/${checkCount} checks, errors ${errors}` };
71
+ }
72
+ if (!SHA256_RE.test(artifactSha) || !SHA256_RE.test(specSha)) {
73
+ return { ok: false, error: 'deliver receipt missing sha256 evidence' };
74
+ }
75
+ return {
76
+ ok: true,
77
+ checksPassed,
78
+ checkCount,
79
+ errors: 0,
80
+ warnings: typeof validation.warnings === 'number' ? validation.warnings : 0,
81
+ artifactSha256: artifactSha,
82
+ specificationSha256: specSha,
83
+ };
84
+ }
85
+
86
+ export function mergeArchitectureState(state, receipt) {
87
+ const base = state && typeof state === 'object' ? state : {};
88
+ const artifacts = base.artifacts && typeof base.artifacts === 'object' ? { ...base.artifacts } : {};
89
+ if (receipt && receipt.ok === true) {
90
+ artifacts[ARCHITECTURE_STATE_KEY] = ARCHITECTURE_HTML_PATH;
91
+ return { state: { ...base, artifacts }, escalate: null };
92
+ }
93
+ const detail = receipt && typeof receipt.error === 'string' && receipt.error ? receipt.error : 'archify deliver/validate failed';
94
+ return {
95
+ state: { ...base, artifacts },
96
+ escalate: `archify deliver/validate failed: ${String(detail).slice(0, 200)}, previous artifact preserved`,
97
+ };
98
+ }
99
+
100
+ export function buildUrlLine() {
101
+ return `url: ${ARCHITECTURE_HTML_PATH}`;
102
+ }
103
+
104
+ export function planCanvasCommands() {
105
+ return {
106
+ open: `aos-plan-canvas open ${ARCHITECTURE_HTML_PATH}`,
107
+ awaitCmd: `aos-plan-canvas await ${ARCHITECTURE_HTML_PATH}`,
108
+ };
109
+ }
110
+
111
+ export function agentTrailCommand() {
112
+ return 'aos-trail . --plan production_artifacts/00_execution_plan.md --no-open';
113
+ }
114
+
115
+ export function buildPlanCanvasOpenArgs(htmlPath) {
116
+ if (!isAllowedArchitecturePath(htmlPath)) return null;
117
+ if (htmlPath !== ARCHITECTURE_HTML_PATH) return null;
118
+ return ['open', ARCHITECTURE_HTML_PATH, '--no-open'];
119
+ }
120
+
121
+ export function buildAgentTrailArgs() {
122
+ return ['.', '--plan', EXECUTION_PLAN_PATH, '--no-open'];
123
+ }
124
+
125
+ export async function runArchitectureCanvas(options = {}) {
126
+ const cwd = options.cwd;
127
+ const htmlPath = options.htmlPath;
128
+ const exists = options.exists;
129
+ const run = options.run;
130
+ const maxBytes = Number.isInteger(options.maxBytes) ? options.maxBytes : PLAN_CANVAS_MAX_BYTES;
131
+ const timeoutMs = Number.isInteger(options.timeoutMs) ? options.timeoutMs : PLAN_CANVAS_TIMEOUT_MS;
132
+ const args = buildPlanCanvasOpenArgs(htmlPath);
133
+ if (!args || typeof cwd !== 'string' || cwd.length === 0) {
134
+ return { displayed: false, reason: 'plan-canvas rejected untrusted arguments, architecture preserved', architecturePreserved: true };
135
+ }
136
+ if (typeof exists === 'function') {
137
+ let present = false;
138
+ try {
139
+ present = exists(htmlPath);
140
+ } catch {
141
+ present = false;
142
+ }
143
+ if (!present) {
144
+ return { displayed: false, reason: 'architecture artifact missing, canvas skipped', architecturePreserved: true };
145
+ }
146
+ }
147
+ if (typeof run !== 'function') {
148
+ return { displayed: false, reason: 'plan-canvas unavailable, architecture preserved', architecturePreserved: true };
149
+ }
150
+ let res = null;
151
+ try {
152
+ res = await run(args, { cwd, timeoutMs, shell: false });
153
+ } catch (error) {
154
+ return { displayed: false, error: redactForLog(error && error.message ? error.message : 'plan-canvas open failed'), architecturePreserved: true };
155
+ }
156
+ const out = boundedOutputText(res ? res.stdout : '', maxBytes);
157
+ if (!out.ok) {
158
+ return { displayed: false, error: 'plan-canvas output exceeds bounded size', architecturePreserved: true };
159
+ }
160
+ if (!res || res.status !== 0) {
161
+ return { displayed: false, error: redactForLog((res && res.stderr) || 'plan-canvas open failed'), architecturePreserved: true };
162
+ }
163
+ return { displayed: true, architecturePreserved: true };
164
+ }
165
+
166
+ export function redactForLog(value) {
167
+ const text = String(value ?? '');
168
+ const homes = text.replace(HOME_RE, '~');
169
+ const shas = homes.replace(LONG_HEX_RE, (m) => `${m.slice(0, 12)}..`);
170
+ return shas.slice(0, LOG_MAX_CHARS);
171
+ }
172
+
173
+ function isWithin(root, candidate) {
174
+ const relativePath = path.relative(root, candidate);
175
+ return relativePath === '' || (relativePath !== '..' && !relativePath.startsWith(`..${path.sep}`) && !path.isAbsolute(relativePath));
176
+ }
177
+
178
+ export function resolveArchifyBin(cwd) {
179
+ if (typeof cwd !== 'string' || cwd.length === 0) return null;
180
+ try {
181
+ const root = fs.realpathSync(path.resolve(cwd));
182
+ if (!fs.statSync(root).isDirectory()) return null;
183
+ const binaryPath = fs.realpathSync(path.resolve(root, ARCHIFY_REL_BIN));
184
+ if (!isWithin(root, binaryPath) || !fs.statSync(binaryPath).isFile()) return null;
185
+ return binaryPath;
186
+ } catch {
187
+ return null;
188
+ }
189
+ }
190
+
191
+ export function buildValidateArgs(specPath) {
192
+ if (!isAllowedArchitecturePath(specPath)) return null;
193
+ if (specPath !== ARCHITECTURE_SPEC_PATH) return null;
194
+ return ['validate', 'architecture', specPath, '--quality', 'showcase', '--json'];
195
+ }
196
+
197
+ export function buildDeliverArgs(specPath, htmlPath) {
198
+ if (!isAllowedArchitecturePath(specPath) || !isAllowedArchitecturePath(htmlPath)) return null;
199
+ if (specPath !== ARCHITECTURE_SPEC_PATH || htmlPath !== ARCHITECTURE_HTML_PATH) return null;
200
+ return ['deliver', 'architecture', specPath, htmlPath, '--quality', 'showcase', '--json'];
201
+ }
202
+
203
+ function boundedOutputText(value, maxBytes) {
204
+ if (typeof value !== 'string') return { ok: false, text: '' };
205
+ let size = 0;
206
+ try {
207
+ size = Buffer.byteLength(value, 'utf8');
208
+ } catch {
209
+ size = value.length;
210
+ }
211
+ if (size > maxBytes) return { ok: false, text: '' };
212
+ return { ok: true, text: value };
213
+ }
214
+
215
+ function boundaryFailure(stage, detail) {
216
+ return {
217
+ ok: false,
218
+ stage,
219
+ error: redactForLog(String(detail || 'archify boundary step failed')).slice(0, LOG_MAX_CHARS),
220
+ preservePrevious: true,
221
+ };
222
+ }
223
+
224
+ export async function runArchifyBoundary(options = {}) {
225
+ const cwd = options.cwd;
226
+ const specPath = options.specPath;
227
+ const htmlPath = options.htmlPath;
228
+ const run = options.run;
229
+ const maxBytes = Number.isInteger(options.maxBytes) ? options.maxBytes : ARCHIFY_MAX_BYTES;
230
+ const timeoutMs = Number.isInteger(options.timeoutMs) ? options.timeoutMs : ARCHIFY_TIMEOUT_MS;
231
+ const validateArgs = buildValidateArgs(specPath);
232
+ const deliverArgs = buildDeliverArgs(specPath, htmlPath);
233
+ if (!validateArgs || !deliverArgs || typeof cwd !== 'string' || cwd.length === 0) {
234
+ return boundaryFailure('arguments', 'archify boundary rejected untrusted arguments');
235
+ }
236
+ if (typeof run !== 'function') {
237
+ return boundaryFailure('executor', 'archify boundary has no executor in this runtime');
238
+ }
239
+ let validateRes = null;
240
+ try {
241
+ validateRes = await run(validateArgs, { cwd, timeoutMs, shell: false, maxBuffer: maxBytes });
242
+ } catch (error) {
243
+ return boundaryFailure('validate', error && error.message ? error.message : 'archify validate executor failed');
244
+ }
245
+ const validateOut = boundedOutputText(validateRes ? validateRes.stdout : '', maxBytes);
246
+ if (!validateRes || validateRes.status !== 0 || !validateOut.ok) {
247
+ const detail = !validateOut.ok
248
+ ? 'archify validate output exceeds bounded size'
249
+ : (validateRes && validateRes.stderr) || 'archify validate failed';
250
+ return boundaryFailure('validate', detail);
251
+ }
252
+ let deliverRes = null;
253
+ try {
254
+ deliverRes = await run(deliverArgs, { cwd, timeoutMs, shell: false, maxBuffer: maxBytes });
255
+ } catch (error) {
256
+ return boundaryFailure('deliver', error && error.message ? error.message : 'archify deliver executor failed');
257
+ }
258
+ const deliverOut = boundedOutputText(deliverRes ? deliverRes.stdout : '', maxBytes);
259
+ if (!deliverRes || deliverRes.status !== 0 || !deliverOut.ok) {
260
+ const detail = !deliverOut.ok
261
+ ? 'archify deliver output exceeds bounded size'
262
+ : (deliverRes && deliverRes.stderr) || 'archify deliver failed';
263
+ return boundaryFailure('deliver', detail);
264
+ }
265
+ const receipt = parseDeliverReceipt(deliverOut.text, { maxBytes });
266
+ if (!receipt || receipt.ok !== true) {
267
+ return boundaryFailure('deliver', (receipt && receipt.error) || 'archify deliver receipt invalid');
268
+ }
269
+ return {
270
+ ok: true,
271
+ stage: 'deliver',
272
+ validate: { status: 0 },
273
+ deliver: { status: 0 },
274
+ receipt,
275
+ preservePrevious: false,
276
+ };
277
+ }