agentxchain 2.18.0 → 2.19.0
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/README.md +11 -0
- package/bin/agentxchain.js +8 -0
- package/package.json +1 -1
- package/scripts/release-downstream-truth.sh +28 -2
- package/src/commands/demo.js +632 -0
- package/src/lib/repo-observer.js +26 -6
package/README.md
CHANGED
|
@@ -17,6 +17,16 @@ Legacy IDE-window coordination is still shipped as a compatibility mode for team
|
|
|
17
17
|
- [Build your own runner](https://agentxchain.dev/docs/build-your-own-runner/)
|
|
18
18
|
- [Why governed multi-agent delivery matters](https://agentxchain.dev/why/)
|
|
19
19
|
|
|
20
|
+
## Try It Now
|
|
21
|
+
|
|
22
|
+
See governance before you scaffold a real repo:
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
npx agentxchain demo
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Requires Node.js 18.17+ or 20.5+ and `git`. The demo creates a temporary governed repo, runs a full PM -> Dev -> QA lifecycle through the real runner interface, shows gates/decisions/objections, and removes the temp workspace when finished. No API keys, config edits, or manual turn authoring required.
|
|
29
|
+
|
|
20
30
|
## Install
|
|
21
31
|
|
|
22
32
|
```bash
|
|
@@ -119,6 +129,7 @@ agentxchain step
|
|
|
119
129
|
|
|
120
130
|
| Command | What it does |
|
|
121
131
|
|---|---|
|
|
132
|
+
| `demo` | Run a temporary PM -> Dev -> QA governed lifecycle demo with real gates, decisions, and objections |
|
|
122
133
|
| `init --governed [--dir <path>] [--template <id>]` | Create a governed project, optionally in-place or in an explicit target directory, with project-shape-specific planning artifacts |
|
|
123
134
|
| `migrate` | Convert a legacy v3 project to governed format |
|
|
124
135
|
| `status` | Show current run, template, phase, turn, and approval state |
|
package/bin/agentxchain.js
CHANGED
|
@@ -101,6 +101,7 @@ import { intakeHandoffCommand } from '../src/commands/intake-handoff.js';
|
|
|
101
101
|
import { intakeScanCommand } from '../src/commands/intake-scan.js';
|
|
102
102
|
import { intakeResolveCommand } from '../src/commands/intake-resolve.js';
|
|
103
103
|
import { intakeStatusCommand } from '../src/commands/intake-status.js';
|
|
104
|
+
import { demoCommand } from '../src/commands/demo.js';
|
|
104
105
|
|
|
105
106
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
106
107
|
const pkg = JSON.parse(readFileSync(join(__dirname, '..', 'package.json'), 'utf8'));
|
|
@@ -233,6 +234,13 @@ program
|
|
|
233
234
|
.description('Check local environment and first-run readiness')
|
|
234
235
|
.action(doctorCommand);
|
|
235
236
|
|
|
237
|
+
program
|
|
238
|
+
.command('demo')
|
|
239
|
+
.description('Run a complete governed lifecycle demo (no API keys required)')
|
|
240
|
+
.option('-j, --json', 'Output as JSON')
|
|
241
|
+
.option('-v, --verbose', 'Show stack traces on failure')
|
|
242
|
+
.action(demoCommand);
|
|
243
|
+
|
|
236
244
|
program
|
|
237
245
|
.command('validate')
|
|
238
246
|
.description('Validate project protocol artifacts')
|
package/package.json
CHANGED
|
@@ -48,6 +48,8 @@ fi
|
|
|
48
48
|
|
|
49
49
|
PACKAGE_NAME="$(node -e "console.log(JSON.parse(require('fs').readFileSync('package.json', 'utf8')).name)")"
|
|
50
50
|
CANONICAL_HOMEBREW_FORMULA_URL="${AGENTXCHAIN_DOWNSTREAM_FORMULA_URL:-https://raw.githubusercontent.com/shivamtiwari93/homebrew-tap/main/Formula/agentxchain.rb}"
|
|
51
|
+
CANONICAL_HOMEBREW_FORMULA_REPO="${AGENTXCHAIN_DOWNSTREAM_FORMULA_REPO:-https://github.com/shivamtiwari93/homebrew-tap.git}"
|
|
52
|
+
CANONICAL_HOMEBREW_FORMULA_PATH="${AGENTXCHAIN_DOWNSTREAM_FORMULA_PATH:-Formula/agentxchain.rb}"
|
|
51
53
|
|
|
52
54
|
PASS=0
|
|
53
55
|
FAIL=0
|
|
@@ -55,6 +57,30 @@ FAIL=0
|
|
|
55
57
|
pass() { PASS=$((PASS + 1)); echo " PASS: $1"; }
|
|
56
58
|
fail() { FAIL=$((FAIL + 1)); echo " FAIL: $1"; }
|
|
57
59
|
|
|
60
|
+
fetch_formula_content() {
|
|
61
|
+
local content=""
|
|
62
|
+
if [[ -n "${AGENTXCHAIN_DOWNSTREAM_FORMULA_URL:-}" ]]; then
|
|
63
|
+
content="$(curl -fsSL "$CANONICAL_HOMEBREW_FORMULA_URL" 2>/dev/null || true)"
|
|
64
|
+
printf '%s' "$content"
|
|
65
|
+
return 0
|
|
66
|
+
fi
|
|
67
|
+
|
|
68
|
+
if ! command -v git >/dev/null 2>&1; then
|
|
69
|
+
printf ''
|
|
70
|
+
return 0
|
|
71
|
+
fi
|
|
72
|
+
|
|
73
|
+
local tmpdir
|
|
74
|
+
tmpdir="$(mktemp -d)"
|
|
75
|
+
if git clone --depth 1 "$CANONICAL_HOMEBREW_FORMULA_REPO" "$tmpdir" >/dev/null 2>&1; then
|
|
76
|
+
if [[ -f "$tmpdir/$CANONICAL_HOMEBREW_FORMULA_PATH" ]]; then
|
|
77
|
+
content="$(cat "$tmpdir/$CANONICAL_HOMEBREW_FORMULA_PATH")"
|
|
78
|
+
fi
|
|
79
|
+
fi
|
|
80
|
+
rm -rf "$tmpdir"
|
|
81
|
+
printf '%s' "$content"
|
|
82
|
+
}
|
|
83
|
+
|
|
58
84
|
echo "AgentXchain v${TARGET_VERSION} Downstream Release Truth"
|
|
59
85
|
echo "====================================="
|
|
60
86
|
echo "Checks downstream surfaces after publish: GitHub release, canonical Homebrew tap."
|
|
@@ -87,11 +113,11 @@ fi
|
|
|
87
113
|
# --- Get registry tarball URL and compute SHA ---
|
|
88
114
|
echo "[2/3] Canonical Homebrew tap SHA matches registry tarball"
|
|
89
115
|
REGISTRY_TARBALL_URL="$(npm view "${PACKAGE_NAME}@${TARGET_VERSION}" dist.tarball 2>/dev/null || true)"
|
|
90
|
-
FORMULA_CONTENT="$(
|
|
116
|
+
FORMULA_CONTENT="$(fetch_formula_content)"
|
|
91
117
|
if [[ -z "$REGISTRY_TARBALL_URL" ]]; then
|
|
92
118
|
fail "cannot fetch registry tarball URL for ${PACKAGE_NAME}@${TARGET_VERSION}"
|
|
93
119
|
elif [[ -z "$FORMULA_CONTENT" ]]; then
|
|
94
|
-
fail "cannot fetch canonical Homebrew formula from ${
|
|
120
|
+
fail "cannot fetch canonical Homebrew formula from ${CANONICAL_HOMEBREW_FORMULA_REPO}"
|
|
95
121
|
else
|
|
96
122
|
REGISTRY_SHA="$(curl -sL "$REGISTRY_TARBALL_URL" | shasum -a 256 | awk '{print $1}')"
|
|
97
123
|
if [[ -z "$REGISTRY_SHA" ]]; then
|
|
@@ -0,0 +1,632 @@
|
|
|
1
|
+
import { mkdirSync, writeFileSync, readFileSync, rmSync } from 'fs';
|
|
2
|
+
import { join } from 'path';
|
|
3
|
+
import { tmpdir } from 'os';
|
|
4
|
+
import { randomBytes } from 'crypto';
|
|
5
|
+
import { execSync } from 'child_process';
|
|
6
|
+
import chalk from 'chalk';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* `agentxchain demo` — zero-friction first-run experience.
|
|
10
|
+
*
|
|
11
|
+
* Runs a complete PM → Dev → QA governed lifecycle in a temp dir
|
|
12
|
+
* using programmatically staged turn results. No API keys, no
|
|
13
|
+
* external tools, no manual steps. Shows governance in action.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
// ── Config ──────────────────────────────────────────────────────────────────
|
|
17
|
+
|
|
18
|
+
function makeConfig() {
|
|
19
|
+
return {
|
|
20
|
+
schema_version: 4,
|
|
21
|
+
protocol_mode: 'governed',
|
|
22
|
+
project: { id: 'agentxchain-demo', name: 'AgentXchain Demo', default_branch: 'main' },
|
|
23
|
+
roles: {
|
|
24
|
+
pm: {
|
|
25
|
+
title: 'Product Manager',
|
|
26
|
+
mandate: 'Protect user value, scope clarity, and acceptance criteria.',
|
|
27
|
+
write_authority: 'review_only',
|
|
28
|
+
runtime_class: 'manual',
|
|
29
|
+
runtime_id: 'manual-pm',
|
|
30
|
+
},
|
|
31
|
+
dev: {
|
|
32
|
+
title: 'Developer',
|
|
33
|
+
mandate: 'Implement approved work safely and verify behavior.',
|
|
34
|
+
write_authority: 'authoritative',
|
|
35
|
+
runtime_class: 'manual',
|
|
36
|
+
runtime_id: 'manual-dev',
|
|
37
|
+
},
|
|
38
|
+
qa: {
|
|
39
|
+
title: 'QA Reviewer',
|
|
40
|
+
mandate: 'Challenge correctness, acceptance coverage, and ship readiness.',
|
|
41
|
+
write_authority: 'review_only',
|
|
42
|
+
runtime_class: 'manual',
|
|
43
|
+
runtime_id: 'manual-qa',
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
runtimes: {
|
|
47
|
+
'manual-pm': { type: 'manual' },
|
|
48
|
+
'manual-dev': { type: 'manual' },
|
|
49
|
+
'manual-qa': { type: 'manual' },
|
|
50
|
+
},
|
|
51
|
+
routing: {
|
|
52
|
+
planning: {
|
|
53
|
+
entry_role: 'pm',
|
|
54
|
+
allowed_next_roles: ['pm', 'human'],
|
|
55
|
+
exit_gate: 'planning_signoff',
|
|
56
|
+
},
|
|
57
|
+
implementation: {
|
|
58
|
+
entry_role: 'dev',
|
|
59
|
+
allowed_next_roles: ['dev', 'qa', 'human'],
|
|
60
|
+
exit_gate: 'implementation_complete',
|
|
61
|
+
},
|
|
62
|
+
qa: {
|
|
63
|
+
entry_role: 'qa',
|
|
64
|
+
allowed_next_roles: ['dev', 'qa', 'human'],
|
|
65
|
+
exit_gate: 'qa_ship_verdict',
|
|
66
|
+
},
|
|
67
|
+
},
|
|
68
|
+
gates: {
|
|
69
|
+
planning_signoff: {
|
|
70
|
+
requires_files: ['.planning/PM_SIGNOFF.md'],
|
|
71
|
+
requires_human_approval: true,
|
|
72
|
+
},
|
|
73
|
+
implementation_complete: {
|
|
74
|
+
requires_files: ['.planning/IMPLEMENTATION_NOTES.md'],
|
|
75
|
+
requires_verification_pass: true,
|
|
76
|
+
},
|
|
77
|
+
qa_ship_verdict: {
|
|
78
|
+
requires_files: ['.planning/acceptance-matrix.md', '.planning/ship-verdict.md'],
|
|
79
|
+
requires_human_approval: true,
|
|
80
|
+
},
|
|
81
|
+
},
|
|
82
|
+
budget: { per_turn_max_usd: 1.0, per_run_max_usd: 5.0 },
|
|
83
|
+
rules: { challenge_required: true, max_turn_retries: 2, max_deadlock_cycles: 1 },
|
|
84
|
+
files: {
|
|
85
|
+
talk: 'TALK.md',
|
|
86
|
+
history: '.agentxchain/history.jsonl',
|
|
87
|
+
state: '.agentxchain/state.json',
|
|
88
|
+
},
|
|
89
|
+
compat: {
|
|
90
|
+
next_owner_source: 'state-json',
|
|
91
|
+
lock_based_coordination: false,
|
|
92
|
+
original_version: 4,
|
|
93
|
+
},
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// ── Canned turn results ─────────────────────────────────────────────────────
|
|
98
|
+
|
|
99
|
+
function makePmTurnResult(runId, turnId) {
|
|
100
|
+
return {
|
|
101
|
+
schema_version: '1.0',
|
|
102
|
+
run_id: runId,
|
|
103
|
+
turn_id: turnId,
|
|
104
|
+
role: 'pm',
|
|
105
|
+
runtime_id: 'manual-pm',
|
|
106
|
+
status: 'completed',
|
|
107
|
+
summary: 'Scoped auth token rotation service: key expiry, graceful rollover, and audit logging. Established 3 acceptance criteria with security constraints.',
|
|
108
|
+
decisions: [
|
|
109
|
+
{
|
|
110
|
+
id: 'DEC-001',
|
|
111
|
+
category: 'scope',
|
|
112
|
+
statement: 'MVP scope: single-module token rotation with expiry check, graceful rollover, and audit trail.',
|
|
113
|
+
rationale: 'Security-sensitive surface — minimal scope reduces attack surface while proving governance value.',
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
id: 'DEC-002',
|
|
117
|
+
category: 'scope',
|
|
118
|
+
statement: 'Three acceptance criteria: safe rotation with rollback, monotonic expiry checks, and audit log on every lifecycle event.',
|
|
119
|
+
rationale: 'Each criterion maps to a testable assertion. Compliance requires traceability.',
|
|
120
|
+
},
|
|
121
|
+
],
|
|
122
|
+
objections: [
|
|
123
|
+
{
|
|
124
|
+
id: 'OBJ-001',
|
|
125
|
+
severity: 'high',
|
|
126
|
+
statement: 'No rollback plan if new tokens fail validation. Live API keys could be invalidated without a recovery path.',
|
|
127
|
+
status: 'raised',
|
|
128
|
+
},
|
|
129
|
+
],
|
|
130
|
+
files_changed: [],
|
|
131
|
+
artifacts_created: [],
|
|
132
|
+
verification: {
|
|
133
|
+
status: 'pass',
|
|
134
|
+
commands: [],
|
|
135
|
+
evidence_summary: 'Planning review — no code to verify.',
|
|
136
|
+
machine_evidence: [],
|
|
137
|
+
},
|
|
138
|
+
artifact: { type: 'review', ref: null },
|
|
139
|
+
proposed_next_role: 'human',
|
|
140
|
+
phase_transition_request: 'implementation',
|
|
141
|
+
needs_human_reason: null,
|
|
142
|
+
cost: { input_tokens: 0, output_tokens: 0, usd: 0 },
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function makeDevTurnResult(runId, turnId) {
|
|
147
|
+
return {
|
|
148
|
+
schema_version: '1.0',
|
|
149
|
+
run_id: runId,
|
|
150
|
+
turn_id: turnId,
|
|
151
|
+
role: 'dev',
|
|
152
|
+
runtime_id: 'manual-dev',
|
|
153
|
+
status: 'completed',
|
|
154
|
+
summary: 'Implemented token rotation with rollback, monotonic expiry, and audit trail. All 3 tests passing.',
|
|
155
|
+
decisions: [
|
|
156
|
+
{
|
|
157
|
+
id: 'DEC-003',
|
|
158
|
+
category: 'implementation',
|
|
159
|
+
statement: 'Added atomic rollback: new token is validated before old token is invalidated.',
|
|
160
|
+
rationale: 'Addresses OBJ-001 — live keys are never invalidated without a validated replacement.',
|
|
161
|
+
},
|
|
162
|
+
],
|
|
163
|
+
objections: [
|
|
164
|
+
{
|
|
165
|
+
id: 'OBJ-002',
|
|
166
|
+
severity: 'medium',
|
|
167
|
+
statement: 'Token expiry check uses wall-clock time without monotonic fallback. Clock skew could skip rotation or double-rotate.',
|
|
168
|
+
status: 'raised',
|
|
169
|
+
},
|
|
170
|
+
],
|
|
171
|
+
files_changed: ['token-rotator.js', 'token-rotator.test.js', '.planning/IMPLEMENTATION_NOTES.md'],
|
|
172
|
+
artifacts_created: [],
|
|
173
|
+
verification: {
|
|
174
|
+
status: 'pass',
|
|
175
|
+
commands: ['node token-rotator.test.js'],
|
|
176
|
+
evidence_summary: '3/3 tests passing: safe rotation with rollback, expiry bounds, audit emission.',
|
|
177
|
+
machine_evidence: [
|
|
178
|
+
{ command: 'node token-rotator.test.js', exit_code: 0, stdout_excerpt: '3 tests passed, 0 failed' },
|
|
179
|
+
],
|
|
180
|
+
},
|
|
181
|
+
artifact: { type: 'commit', ref: 'token-rotator.js' },
|
|
182
|
+
proposed_next_role: 'qa',
|
|
183
|
+
phase_transition_request: 'qa',
|
|
184
|
+
needs_human_reason: null,
|
|
185
|
+
cost: { input_tokens: 0, output_tokens: 0, usd: 0 },
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function makeQaTurnResult(runId, turnId) {
|
|
190
|
+
return {
|
|
191
|
+
schema_version: '1.0',
|
|
192
|
+
run_id: runId,
|
|
193
|
+
turn_id: turnId,
|
|
194
|
+
role: 'qa',
|
|
195
|
+
runtime_id: 'manual-qa',
|
|
196
|
+
status: 'completed',
|
|
197
|
+
summary: 'Reviewed token rotation against acceptance matrix. All 3 criteria met. Ship verdict: YES.',
|
|
198
|
+
decisions: [
|
|
199
|
+
{
|
|
200
|
+
id: 'DEC-004',
|
|
201
|
+
category: 'quality',
|
|
202
|
+
statement: 'All acceptance criteria verified: rollback safety, expiry monotonicity, and audit completeness.',
|
|
203
|
+
rationale: 'Token rotation, rollback, and audit trail all function as specified.',
|
|
204
|
+
},
|
|
205
|
+
{
|
|
206
|
+
id: 'DEC-005',
|
|
207
|
+
category: 'release',
|
|
208
|
+
statement: 'Ship verdict: YES. Security-sensitive implementation meets all acceptance criteria.',
|
|
209
|
+
rationale: 'OBJ-002 (clock skew) is noted for follow-up but not blocking for controlled environments.',
|
|
210
|
+
},
|
|
211
|
+
],
|
|
212
|
+
objections: [
|
|
213
|
+
{
|
|
214
|
+
id: 'OBJ-003',
|
|
215
|
+
severity: 'medium',
|
|
216
|
+
statement: 'No audit entry emitted on rotation failure. Compliance requires traceability for every key lifecycle event.',
|
|
217
|
+
status: 'raised',
|
|
218
|
+
},
|
|
219
|
+
],
|
|
220
|
+
files_changed: [],
|
|
221
|
+
artifacts_created: [],
|
|
222
|
+
verification: {
|
|
223
|
+
status: 'pass',
|
|
224
|
+
commands: [],
|
|
225
|
+
evidence_summary: 'Review-only turn. Verified against acceptance matrix.',
|
|
226
|
+
machine_evidence: [],
|
|
227
|
+
},
|
|
228
|
+
artifact: { type: 'review', ref: null },
|
|
229
|
+
proposed_next_role: 'human',
|
|
230
|
+
phase_transition_request: null,
|
|
231
|
+
run_completion_request: true,
|
|
232
|
+
needs_human_reason: null,
|
|
233
|
+
cost: { input_tokens: 0, output_tokens: 0, usd: 0 },
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// ── Scaffold helpers ────────────────────────────────────────────────────────
|
|
238
|
+
|
|
239
|
+
function scaffoldProject(root) {
|
|
240
|
+
const config = makeConfig();
|
|
241
|
+
writeFileSync(join(root, 'agentxchain.json'), JSON.stringify(config, null, 2));
|
|
242
|
+
mkdirSync(join(root, '.agentxchain/prompts'), { recursive: true });
|
|
243
|
+
mkdirSync(join(root, '.planning'), { recursive: true });
|
|
244
|
+
|
|
245
|
+
writeFileSync(join(root, '.agentxchain/state.json'), JSON.stringify({
|
|
246
|
+
schema_version: '1.1',
|
|
247
|
+
status: 'idle',
|
|
248
|
+
phase: 'planning',
|
|
249
|
+
run_id: null,
|
|
250
|
+
active_turns: {},
|
|
251
|
+
next_role: null,
|
|
252
|
+
pending_phase_transition: null,
|
|
253
|
+
pending_run_completion: null,
|
|
254
|
+
blocked_on: null,
|
|
255
|
+
blocked_reason: null,
|
|
256
|
+
}, null, 2));
|
|
257
|
+
|
|
258
|
+
writeFileSync(join(root, '.agentxchain/history.jsonl'), '');
|
|
259
|
+
writeFileSync(join(root, '.agentxchain/decision-ledger.jsonl'), '');
|
|
260
|
+
writeFileSync(join(root, '.agentxchain/prompts/pm.md'), '# PM Prompt\nYou are the Product Manager.');
|
|
261
|
+
writeFileSync(join(root, '.agentxchain/prompts/dev.md'), '# Dev Prompt\nYou are the Developer.');
|
|
262
|
+
writeFileSync(join(root, '.agentxchain/prompts/qa.md'), '# QA Prompt\nYou are the QA Reviewer.');
|
|
263
|
+
writeFileSync(join(root, 'TALK.md'), '# Collaboration Log\n');
|
|
264
|
+
|
|
265
|
+
// Planning artifacts — PM_SIGNOFF starts blocked (flipped after PM turn)
|
|
266
|
+
writeFileSync(join(root, '.planning/PM_SIGNOFF.md'), '# PM Planning Sign-Off\n\nApproved: NO\n');
|
|
267
|
+
writeFileSync(join(root, '.planning/ROADMAP.md'), '# Roadmap\n\n(PM fills this)\n');
|
|
268
|
+
|
|
269
|
+
return config;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function gitInit(root) {
|
|
273
|
+
execSync('git init', { cwd: root, stdio: 'ignore' });
|
|
274
|
+
execSync('git config user.email "demo@agentxchain.dev"', { cwd: root, stdio: 'ignore' });
|
|
275
|
+
execSync('git config user.name "AgentXchain Demo"', { cwd: root, stdio: 'ignore' });
|
|
276
|
+
execSync('git add -A', { cwd: root, stdio: 'ignore' });
|
|
277
|
+
execSync('git commit -m "demo: scaffold governed project"', { cwd: root, stdio: 'ignore' });
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function gitCommit(root, message) {
|
|
281
|
+
execSync('git add -A', { cwd: root, stdio: 'ignore' });
|
|
282
|
+
execSync(`git commit -m "${message}" --allow-empty`, { cwd: root, stdio: 'ignore' });
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function stageTurnResult(root, turnId, result) {
|
|
286
|
+
const stagingDir = join(root, '.agentxchain/staging', turnId);
|
|
287
|
+
mkdirSync(stagingDir, { recursive: true });
|
|
288
|
+
writeFileSync(join(stagingDir, 'turn-result.json'), JSON.stringify(result, null, 2));
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// ── Output helpers ──────────────────────────────────────────────────────────
|
|
292
|
+
|
|
293
|
+
function header(text) {
|
|
294
|
+
console.log('');
|
|
295
|
+
console.log(chalk.bold.cyan(` ── ${text} ──`));
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function step(text) {
|
|
299
|
+
console.log(chalk.dim(' ▸ ') + text);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function lesson(text) {
|
|
303
|
+
console.log(chalk.dim(' → ') + chalk.italic(text));
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
function success(text) {
|
|
307
|
+
console.log(chalk.dim(' ▸ ') + chalk.green(text));
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// ── Main ────────────────────────────────────────────────────────────────────
|
|
311
|
+
|
|
312
|
+
export async function demoCommand(opts = {}) {
|
|
313
|
+
const jsonMode = opts.json || false;
|
|
314
|
+
const verbose = opts.verbose || false;
|
|
315
|
+
const startTime = Date.now();
|
|
316
|
+
|
|
317
|
+
const root = join(tmpdir(), `agentxchain-demo-${randomBytes(6).toString('hex')}`);
|
|
318
|
+
mkdirSync(root, { recursive: true });
|
|
319
|
+
|
|
320
|
+
const result = {
|
|
321
|
+
ok: false,
|
|
322
|
+
run_id: null,
|
|
323
|
+
turns: [],
|
|
324
|
+
decisions: 0,
|
|
325
|
+
objections: 0,
|
|
326
|
+
duration_ms: 0,
|
|
327
|
+
error: null,
|
|
328
|
+
};
|
|
329
|
+
|
|
330
|
+
try {
|
|
331
|
+
// Verify git is available
|
|
332
|
+
try {
|
|
333
|
+
execSync('git --version', { stdio: 'ignore' });
|
|
334
|
+
} catch {
|
|
335
|
+
throw new Error('git is required for the demo but was not found in PATH');
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// Lazy-load runner interface to avoid circular imports at module level
|
|
339
|
+
const {
|
|
340
|
+
initRun,
|
|
341
|
+
assignTurn,
|
|
342
|
+
acceptTurn,
|
|
343
|
+
approvePhaseGate,
|
|
344
|
+
approveCompletionGate,
|
|
345
|
+
} = await import('../lib/runner-interface.js');
|
|
346
|
+
|
|
347
|
+
if (!jsonMode) {
|
|
348
|
+
console.log('');
|
|
349
|
+
console.log(chalk.bold(' AgentXchain Demo — Governed Multi-Agent Delivery'));
|
|
350
|
+
console.log(chalk.dim(' ' + '─'.repeat(51)));
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// ── Scaffold ──────────────────────────────────────────────────────────
|
|
354
|
+
if (!jsonMode) step('Scaffolding governed project...');
|
|
355
|
+
const config = scaffoldProject(root);
|
|
356
|
+
gitInit(root);
|
|
357
|
+
|
|
358
|
+
// ── Init run ──────────────────────────────────────────────────────────
|
|
359
|
+
const runResult = initRun(root, config);
|
|
360
|
+
if (!runResult.ok) throw new Error(`initRun failed: ${runResult.error}`);
|
|
361
|
+
const runId = runResult.state.run_id;
|
|
362
|
+
result.run_id = runId;
|
|
363
|
+
|
|
364
|
+
if (!jsonMode) step(`Starting governed run: ${chalk.bold(runId.slice(0, 16))}...`);
|
|
365
|
+
|
|
366
|
+
// ── PM Turn (Planning) ────────────────────────────────────────────────
|
|
367
|
+
if (!jsonMode) header('PM Turn — Planning Phase');
|
|
368
|
+
|
|
369
|
+
const pmAssign = assignTurn(root, config, 'pm');
|
|
370
|
+
if (!pmAssign.ok) throw new Error(`PM assign failed: ${pmAssign.error}`);
|
|
371
|
+
const pmTurnId = pmAssign.turn.turn_id;
|
|
372
|
+
|
|
373
|
+
if (!jsonMode) step(`Assigned PM turn: ${chalk.dim(pmTurnId.slice(0, 16))}...`);
|
|
374
|
+
|
|
375
|
+
// Stage PM turn result
|
|
376
|
+
const pmResult = makePmTurnResult(runId, pmTurnId);
|
|
377
|
+
stageTurnResult(root, pmTurnId, pmResult);
|
|
378
|
+
|
|
379
|
+
if (!jsonMode) {
|
|
380
|
+
step('PM scoped auth token rotation: key expiry, graceful rollover, audit trail');
|
|
381
|
+
step(`PM raised ${chalk.yellow('1 objection')}: "No rollback plan — live API keys could be invalidated without recovery"`);
|
|
382
|
+
lesson('Without mandatory challenge, this missing rollback plan would have reached implementation unchecked');
|
|
383
|
+
step(`PM recorded ${chalk.blue('2 decisions')} in the decision ledger`);
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// Write planning artifacts BEFORE acceptance
|
|
387
|
+
writeFileSync(join(root, '.planning/ROADMAP.md'),
|
|
388
|
+
'# Roadmap\n\n## Acceptance Criteria\n\n1. Token rotation with atomic rollback — old key stays valid until new key is verified\n2. Expiry checks use monotonic time — no clock-skew-induced double-rotation\n3. Audit log emitted on every key lifecycle event (create, rotate, expire, revoke)\n');
|
|
389
|
+
writeFileSync(join(root, '.planning/PM_SIGNOFF.md'),
|
|
390
|
+
'# PM Planning Sign-Off\n\nApproved: YES\n');
|
|
391
|
+
gitCommit(root, 'demo: pm planning work');
|
|
392
|
+
|
|
393
|
+
const pmAccept = acceptTurn(root, config);
|
|
394
|
+
if (!pmAccept.ok) throw new Error(`PM accept failed: ${pmAccept.error}`);
|
|
395
|
+
gitCommit(root, 'demo: accept pm turn');
|
|
396
|
+
|
|
397
|
+
if (!jsonMode) success('Turn accepted ✓');
|
|
398
|
+
result.turns.push({ role: 'pm', turn_id: pmTurnId, phase: 'planning' });
|
|
399
|
+
result.decisions += pmResult.decisions.length;
|
|
400
|
+
result.objections += pmResult.objections.length;
|
|
401
|
+
|
|
402
|
+
// ── Phase Gate: planning → implementation ─────────────────────────────
|
|
403
|
+
if (!jsonMode) header('Phase Gate — planning → implementation');
|
|
404
|
+
|
|
405
|
+
const gateResult = approvePhaseGate(root, config);
|
|
406
|
+
if (!gateResult.ok) throw new Error(`Phase gate failed: ${gateResult.error}`);
|
|
407
|
+
gitCommit(root, 'demo: approve phase transition');
|
|
408
|
+
|
|
409
|
+
if (!jsonMode) {
|
|
410
|
+
success('Gate passed: PM_SIGNOFF.md contains "Approved: YES"');
|
|
411
|
+
lesson('This gate stopped 3 AI agents from proceeding until a human confirmed the security scope was correct');
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
// ── Dev Turn (Implementation) ─────────────────────────────────────────
|
|
415
|
+
if (!jsonMode) header('Dev Turn — Implementation Phase');
|
|
416
|
+
|
|
417
|
+
const devAssign = assignTurn(root, config, 'dev');
|
|
418
|
+
if (!devAssign.ok) throw new Error(`Dev assign failed: ${devAssign.error}`);
|
|
419
|
+
const devTurnId = devAssign.turn.turn_id;
|
|
420
|
+
|
|
421
|
+
if (!jsonMode) step(`Assigned Dev turn: ${chalk.dim(devTurnId.slice(0, 16))}...`);
|
|
422
|
+
|
|
423
|
+
// Write implementation files
|
|
424
|
+
writeFileSync(join(root, 'token-rotator.js'), `// Auth Token Rotation Service — governed implementation
|
|
425
|
+
const audit = [];
|
|
426
|
+
let currentToken = { key: 'tok_initial', created: Date.now(), expires: Date.now() + 3600000 };
|
|
427
|
+
let previousToken = null;
|
|
428
|
+
|
|
429
|
+
function rotate(newKey) {
|
|
430
|
+
if (!newKey || typeof newKey !== 'string') {
|
|
431
|
+
audit.push({ event: 'rotate_failed', reason: 'invalid_key', ts: Date.now() });
|
|
432
|
+
throw new Error('Invalid token key: must be a non-empty string');
|
|
433
|
+
}
|
|
434
|
+
// Atomic rollback: validate new token before invalidating old
|
|
435
|
+
const candidate = { key: newKey, created: Date.now(), expires: Date.now() + 3600000 };
|
|
436
|
+
previousToken = currentToken; // preserve rollback path
|
|
437
|
+
currentToken = candidate;
|
|
438
|
+
audit.push({ event: 'rotated', from: previousToken.key, to: newKey, ts: Date.now() });
|
|
439
|
+
return currentToken;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
function rollback() {
|
|
443
|
+
if (!previousToken) throw new Error('No previous token to roll back to');
|
|
444
|
+
const rolled = previousToken;
|
|
445
|
+
currentToken = previousToken;
|
|
446
|
+
previousToken = null;
|
|
447
|
+
audit.push({ event: 'rollback', to: rolled.key, ts: Date.now() });
|
|
448
|
+
return currentToken;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
function getAuditLog() { return [...audit]; }
|
|
452
|
+
|
|
453
|
+
module.exports = { rotate, rollback, getAuditLog, getCurrent: () => currentToken };
|
|
454
|
+
`);
|
|
455
|
+
|
|
456
|
+
writeFileSync(join(root, 'token-rotator.test.js'), `const assert = require('assert');
|
|
457
|
+
const { rotate, rollback, getAuditLog } = require('./token-rotator');
|
|
458
|
+
|
|
459
|
+
// Test 1: Safe rotation with rollback
|
|
460
|
+
const newToken = rotate('tok_v2');
|
|
461
|
+
assert.strictEqual(newToken.key, 'tok_v2');
|
|
462
|
+
const rolledBack = rollback();
|
|
463
|
+
assert.strictEqual(rolledBack.key, 'tok_initial');
|
|
464
|
+
|
|
465
|
+
// Test 2: Invalid key rejected with audit trail
|
|
466
|
+
try { rotate(''); assert.fail('Should throw'); }
|
|
467
|
+
catch (e) { assert.match(e.message, /Invalid token key/); }
|
|
468
|
+
|
|
469
|
+
// Test 3: Audit log captures all lifecycle events
|
|
470
|
+
const log = getAuditLog();
|
|
471
|
+
assert.ok(log.some(e => e.event === 'rotated'), 'rotation logged');
|
|
472
|
+
assert.ok(log.some(e => e.event === 'rollback'), 'rollback logged');
|
|
473
|
+
assert.ok(log.some(e => e.event === 'rotate_failed'), 'failure logged');
|
|
474
|
+
console.log('3 tests passed, 0 failed');
|
|
475
|
+
`);
|
|
476
|
+
|
|
477
|
+
writeFileSync(join(root, '.planning/IMPLEMENTATION_NOTES.md'), `# Implementation Notes
|
|
478
|
+
|
|
479
|
+
## Changes
|
|
480
|
+
|
|
481
|
+
- Created \`token-rotator.js\` with atomic rollback, expiry, and audit logging
|
|
482
|
+
- Created \`token-rotator.test.js\` with 3 test cases covering all acceptance criteria
|
|
483
|
+
- Resolved OBJ-001: new tokens are validated before old tokens are invalidated
|
|
484
|
+
|
|
485
|
+
## Verification
|
|
486
|
+
|
|
487
|
+
- \`node token-rotator.test.js\` → 3/3 passing
|
|
488
|
+
`);
|
|
489
|
+
gitCommit(root, 'demo: dev implementation');
|
|
490
|
+
|
|
491
|
+
// Stage dev turn result
|
|
492
|
+
const devResult = makeDevTurnResult(runId, devTurnId);
|
|
493
|
+
stageTurnResult(root, devTurnId, devResult);
|
|
494
|
+
|
|
495
|
+
if (!jsonMode) {
|
|
496
|
+
step('Dev implemented token rotation with atomic rollback and audit trail');
|
|
497
|
+
step(`Dev resolved PM objection: ${chalk.green('OBJ-001 — rollback path now implemented')}`);
|
|
498
|
+
step(`Dev raised ${chalk.yellow('1 new objection')}: "Clock skew could skip rotation or double-rotate"`);
|
|
499
|
+
lesson('The dev caught a clock-skew bug the PM missed. Independent challenge surfaces different failure classes');
|
|
500
|
+
step(`Verification: ${chalk.green('3/3 tests passing')}`);
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
const devAccept = acceptTurn(root, config);
|
|
504
|
+
if (!devAccept.ok) throw new Error(`Dev accept failed: ${devAccept.error}`);
|
|
505
|
+
gitCommit(root, 'demo: accept dev turn');
|
|
506
|
+
|
|
507
|
+
if (!jsonMode) success('Turn accepted ✓');
|
|
508
|
+
result.turns.push({ role: 'dev', turn_id: devTurnId, phase: 'implementation' });
|
|
509
|
+
result.decisions += devResult.decisions.length;
|
|
510
|
+
result.objections += devResult.objections.length;
|
|
511
|
+
|
|
512
|
+
// implementation_complete gate auto-advances (no requires_human_approval)
|
|
513
|
+
// so the phase is already 'qa' after dev acceptance
|
|
514
|
+
if (!jsonMode) {
|
|
515
|
+
header('Phase Gate — implementation → qa (auto-evaluated)');
|
|
516
|
+
success('Gate passed: IMPLEMENTATION_NOTES.md has real content, verification passed');
|
|
517
|
+
lesson('Without this gate, untested code could reach QA review — wasting a review turn on code that doesn\'t run');
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
// ── QA Turn (Review) ──────────────────────────────────────────────────
|
|
521
|
+
if (!jsonMode) header('QA Turn — Review Phase');
|
|
522
|
+
|
|
523
|
+
const qaAssign = assignTurn(root, config, 'qa');
|
|
524
|
+
if (!qaAssign.ok) throw new Error(`QA assign failed: ${qaAssign.error}`);
|
|
525
|
+
const qaTurnId = qaAssign.turn.turn_id;
|
|
526
|
+
|
|
527
|
+
if (!jsonMode) step(`Assigned QA turn: ${chalk.dim(qaTurnId.slice(0, 16))}...`);
|
|
528
|
+
|
|
529
|
+
// Write QA artifacts
|
|
530
|
+
writeFileSync(join(root, '.planning/acceptance-matrix.md'), `# Acceptance Matrix
|
|
531
|
+
|
|
532
|
+
| Req # | Requirement | Status |
|
|
533
|
+
|-------|-------------|--------|
|
|
534
|
+
| 1 | Token rotation with atomic rollback | PASS |
|
|
535
|
+
| 2 | Monotonic expiry checks | PASS |
|
|
536
|
+
| 3 | Audit log on every lifecycle event | PASS |
|
|
537
|
+
`);
|
|
538
|
+
|
|
539
|
+
writeFileSync(join(root, '.planning/ship-verdict.md'), `# Ship Verdict
|
|
540
|
+
|
|
541
|
+
## Verdict: SHIP
|
|
542
|
+
|
|
543
|
+
All acceptance criteria met. OBJ-002 (clock skew) noted for follow-up. OBJ-003 (failure audit) noted for next sprint.
|
|
544
|
+
`);
|
|
545
|
+
|
|
546
|
+
writeFileSync(join(root, '.planning/RELEASE_NOTES.md'), `# Release Notes — v1.0.0
|
|
547
|
+
|
|
548
|
+
## What shipped
|
|
549
|
+
|
|
550
|
+
- Auth token rotation with atomic rollback and audit trail
|
|
551
|
+
- 3/3 acceptance criteria met
|
|
552
|
+
- Governed delivery: PM → Dev → QA with mandatory challenge at every turn
|
|
553
|
+
- 3 issues caught by governance that would have shipped undetected without challenge
|
|
554
|
+
`);
|
|
555
|
+
gitCommit(root, 'demo: qa review artifacts');
|
|
556
|
+
|
|
557
|
+
// Stage QA turn result
|
|
558
|
+
const qaResult = makeQaTurnResult(runId, qaTurnId);
|
|
559
|
+
stageTurnResult(root, qaTurnId, qaResult);
|
|
560
|
+
|
|
561
|
+
if (!jsonMode) {
|
|
562
|
+
step('QA reviewed token rotation against acceptance matrix');
|
|
563
|
+
step(`QA verdict: ${chalk.green('All 3 criteria PASS')}`);
|
|
564
|
+
step(`QA raised ${chalk.yellow('1 objection')}: "No audit entry on rotation failure — compliance gap"`);
|
|
565
|
+
lesson('QA found a compliance gap neither PM nor dev raised. Three perspectives > one');
|
|
566
|
+
step(`Ship verdict: ${chalk.green('SHIP')}`);
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
const qaAccept = acceptTurn(root, config);
|
|
570
|
+
if (!qaAccept.ok) throw new Error(`QA accept failed: ${qaAccept.error}`);
|
|
571
|
+
gitCommit(root, 'demo: accept qa turn');
|
|
572
|
+
|
|
573
|
+
if (!jsonMode) success('Turn accepted ✓');
|
|
574
|
+
result.turns.push({ role: 'qa', turn_id: qaTurnId, phase: 'qa' });
|
|
575
|
+
result.decisions += qaResult.decisions.length;
|
|
576
|
+
result.objections += qaResult.objections.length;
|
|
577
|
+
|
|
578
|
+
// ── Run Completion ────────────────────────────────────────────────────
|
|
579
|
+
if (!jsonMode) header('Run Completion');
|
|
580
|
+
|
|
581
|
+
const completionResult = approveCompletionGate(root, config);
|
|
582
|
+
if (!completionResult.ok) throw new Error(`Completion failed: ${completionResult.error}`);
|
|
583
|
+
|
|
584
|
+
const completedAt = JSON.parse(readFileSync(join(root, '.agentxchain/state.json'), 'utf8')).completed_at;
|
|
585
|
+
|
|
586
|
+
if (!jsonMode) {
|
|
587
|
+
success(`Approved completion: qa_ship_verdict gate passed`);
|
|
588
|
+
step(`Run completed at ${chalk.dim(completedAt || new Date().toISOString())}`);
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
// ── Summary ───────────────────────────────────────────────────────────
|
|
592
|
+
result.ok = true;
|
|
593
|
+
result.duration_ms = Date.now() - startTime;
|
|
594
|
+
|
|
595
|
+
if (!jsonMode) {
|
|
596
|
+
header('Summary');
|
|
597
|
+
console.log('');
|
|
598
|
+
console.log(` Run: ${chalk.bold(runId.slice(0, 16))}...`);
|
|
599
|
+
console.log(` Turns: ${chalk.bold('3')} (PM, Dev, QA)`);
|
|
600
|
+
console.log(` Decisions: ${chalk.blue(String(result.decisions))} recorded in decision ledger`);
|
|
601
|
+
console.log(` Objections: ${chalk.yellow(String(result.objections))} raised across all turns`);
|
|
602
|
+
console.log(` Duration: ${chalk.dim((result.duration_ms / 1000).toFixed(1) + 's')}`);
|
|
603
|
+
console.log(` Caught: ${chalk.green('3 issues that would have shipped undetected without governed challenge')}`);
|
|
604
|
+
console.log('');
|
|
605
|
+
console.log(chalk.dim(' ─'.repeat(26)));
|
|
606
|
+
console.log('');
|
|
607
|
+
console.log(` ${chalk.bold('Try it for real:')} agentxchain init --governed`);
|
|
608
|
+
console.log(` ${chalk.bold('Read more:')} https://agentxchain.dev/docs/quickstart`);
|
|
609
|
+
console.log('');
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
if (jsonMode) {
|
|
613
|
+
process.stdout.write(JSON.stringify(result, null, 2) + '\n');
|
|
614
|
+
}
|
|
615
|
+
} catch (err) {
|
|
616
|
+
result.ok = false;
|
|
617
|
+
result.error = err.message;
|
|
618
|
+
result.duration_ms = Date.now() - startTime;
|
|
619
|
+
|
|
620
|
+
if (jsonMode) {
|
|
621
|
+
process.stdout.write(JSON.stringify(result, null, 2) + '\n');
|
|
622
|
+
} else {
|
|
623
|
+
console.error(chalk.red(`\n Demo failed: ${err.message}`));
|
|
624
|
+
if (verbose) console.error(chalk.dim(` ${err.stack}`));
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
process.exitCode = 1;
|
|
628
|
+
} finally {
|
|
629
|
+
// Always clean up
|
|
630
|
+
try { rmSync(root, { recursive: true, force: true }); } catch {}
|
|
631
|
+
}
|
|
632
|
+
}
|
package/src/lib/repo-observer.js
CHANGED
|
@@ -39,6 +39,15 @@ const ORCHESTRATOR_STATE_FILES = [
|
|
|
39
39
|
'.agentxchain/lock.json',
|
|
40
40
|
'.agentxchain/hook-audit.jsonl',
|
|
41
41
|
'.agentxchain/hook-annotations.jsonl',
|
|
42
|
+
'TALK.md',
|
|
43
|
+
];
|
|
44
|
+
|
|
45
|
+
// Evidence paths may legitimately remain dirty across turns without blocking the
|
|
46
|
+
// next code-writing assignment. They still remain actor-observable so review
|
|
47
|
+
// accountability is preserved during acceptance.
|
|
48
|
+
const BASELINE_EXEMPT_PATH_PREFIXES = [
|
|
49
|
+
'.agentxchain/reviews/',
|
|
50
|
+
'.agentxchain/reports/',
|
|
42
51
|
];
|
|
43
52
|
|
|
44
53
|
/**
|
|
@@ -50,6 +59,11 @@ export function isOperationalPath(filePath) {
|
|
|
50
59
|
|| ORCHESTRATOR_STATE_FILES.includes(filePath);
|
|
51
60
|
}
|
|
52
61
|
|
|
62
|
+
function isBaselineExemptPath(filePath) {
|
|
63
|
+
return isOperationalPath(filePath)
|
|
64
|
+
|| BASELINE_EXEMPT_PATH_PREFIXES.some(prefix => filePath.startsWith(prefix));
|
|
65
|
+
}
|
|
66
|
+
|
|
53
67
|
// ── Baseline Capture ────────────────────────────────────────────────────────
|
|
54
68
|
|
|
55
69
|
/**
|
|
@@ -57,6 +71,10 @@ export function isOperationalPath(filePath) {
|
|
|
57
71
|
* This gives acceptance a stable "before" view.
|
|
58
72
|
*
|
|
59
73
|
* @param {string} root — project root directory
|
|
74
|
+
* clean is actor-facing baseline cleanliness, not literal `git status` emptiness.
|
|
75
|
+
* dirty_snapshot may still contain baseline-exempt evidence paths so later
|
|
76
|
+
* observation can filter unchanged pre-existing dirt.
|
|
77
|
+
*
|
|
60
78
|
* @returns {{ kind: string, head_ref: string|null, clean: boolean, captured_at: string }}
|
|
61
79
|
*/
|
|
62
80
|
export function captureBaseline(root) {
|
|
@@ -73,14 +91,15 @@ export function captureBaseline(root) {
|
|
|
73
91
|
}
|
|
74
92
|
|
|
75
93
|
const headRef = getHeadRef(root);
|
|
76
|
-
const
|
|
94
|
+
const dirtyFiles = getWorkingTreeChanges(root);
|
|
95
|
+
const clean = dirtyFiles.filter((filePath) => !isBaselineExemptPath(filePath)).length === 0;
|
|
77
96
|
|
|
78
97
|
return {
|
|
79
98
|
kind: 'git_worktree',
|
|
80
99
|
head_ref: headRef,
|
|
81
100
|
clean,
|
|
82
101
|
captured_at: now,
|
|
83
|
-
dirty_snapshot:
|
|
102
|
+
dirty_snapshot: dirtyFiles.length === 0 ? {} : captureDirtyWorkspaceSnapshot(root),
|
|
84
103
|
};
|
|
85
104
|
}
|
|
86
105
|
|
|
@@ -117,7 +136,6 @@ export function observeChanges(root, baseline) {
|
|
|
117
136
|
if (baseline?.head_ref && baseline.head_ref === currentHead) {
|
|
118
137
|
// Same commit — changes are in working tree / staging area
|
|
119
138
|
changedFiles = getWorkingTreeChanges(root);
|
|
120
|
-
changedFiles = filterBaselineDirtyFiles(root, changedFiles, baseline);
|
|
121
139
|
diffSummary = buildObservedDiffSummary(getWorkingTreeDiffSummary(root), untrackedFiles);
|
|
122
140
|
} else if (baseline?.head_ref) {
|
|
123
141
|
// New commits exist — get files changed since baseline ref
|
|
@@ -134,6 +152,8 @@ export function observeChanges(root, baseline) {
|
|
|
134
152
|
diffSummary = buildObservedDiffSummary(getWorkingTreeDiffSummary(root), untrackedFiles);
|
|
135
153
|
}
|
|
136
154
|
|
|
155
|
+
changedFiles = filterBaselineDirtyFiles(root, changedFiles, baseline);
|
|
156
|
+
|
|
137
157
|
// Filter out orchestrator-owned operational paths (Session #19 freeze)
|
|
138
158
|
const actorFiles = changedFiles.filter(f => !isOperationalPath(f));
|
|
139
159
|
|
|
@@ -425,10 +445,10 @@ export function checkCleanBaseline(root, writeAuthority) {
|
|
|
425
445
|
return { clean: true };
|
|
426
446
|
}
|
|
427
447
|
|
|
428
|
-
// Check if all dirty files are orchestrator-owned
|
|
429
|
-
// If only
|
|
448
|
+
// Check if all dirty files are baseline-exempt evidence or orchestrator-owned state.
|
|
449
|
+
// If only those paths are dirty, the baseline is still clean for actor purposes.
|
|
430
450
|
const dirtyFiles = getWorkingTreeChanges(root);
|
|
431
|
-
const actorDirtyFiles = dirtyFiles.filter(f => !
|
|
451
|
+
const actorDirtyFiles = dirtyFiles.filter(f => !isBaselineExemptPath(f));
|
|
432
452
|
|
|
433
453
|
if (actorDirtyFiles.length === 0) return { clean: true };
|
|
434
454
|
|