@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.
- package/.agents/agents.md +8 -3
- package/.agents/nodes.json +2 -1
- package/.agents/state.schema.json +24 -0
- package/.agents/vendor-manifest.json +6 -0
- package/.agents/workflows/startcycle-graph.md +2 -2
- package/.claude/agents/architect.md +5 -4
- package/.claude/agents/techlead.md +2 -0
- package/.claude/workflows/startcycle-dispatch.mjs +175 -17
- package/.opencode/agents/architect.md +5 -4
- package/.opencode/agents/techlead.md +2 -0
- package/THIRD_PARTY_NOTICES.md +9 -1
- package/bin/aos-doctor.mjs +366 -0
- package/bin/aos-store.mjs +167 -0
- package/docs/AUDIT_BRIEF_ECC_STORE.md +122 -0
- package/docs/PLAN_AOS_STORE_PHASE1.md +190 -0
- package/installer.js +41 -1
- package/lib/aos-archify-contract.mjs +277 -0
- package/lib/ecc-store-index.json +1 -0
- package/mcp_config.json +4 -0
- package/mcps/mcsc/packages/core/src/adapters/codex.js +160 -37
- package/package.json +6 -2
- package/scripts/build-ecc-store-index.mjs +147 -0
- package/skills/basic/startcycle-graph/SKILL.md +13 -0
- package/skills/global_config/agenttrail/bin/agenttrail.mjs +153 -42
- package/skills/global_config/agenttrail/public/index.html +5 -4
- package/skills/global_config/archify/examples/dataflow-product-analytics.html +15048 -0
- package/skills/global_config/archify/examples/lifecycle-agent-run.html +14983 -0
- package/skills/global_config/archify/examples/sequence-cache-miss-request.html +15063 -0
- package/skills/global_config/archify/examples/web-app-rendered.html +15012 -0
- package/skills/global_config/archify/examples/workflow-agent-tool-call-rendered.html +15054 -0
- package/skills/global_config/ask-tim/SKILL.md +10 -0
- package/skills/global_config/mcsc/SKILL.md +60 -0
- package/skills/global_config/plan-canvas/scripts/lib/plan-canvas/server.js +31 -6
- 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
|
+
}
|