agentxchain 2.145.0 → 2.147.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.
Files changed (43) hide show
  1. package/dashboard/app.js +3 -0
  2. package/dashboard/components/notifications.js +127 -0
  3. package/dashboard/index.html +1 -0
  4. package/package.json +1 -1
  5. package/scripts/publish-npm.sh +16 -0
  6. package/scripts/release-downstream-truth.sh +16 -8
  7. package/scripts/sync-homebrew.sh +14 -1
  8. package/scripts/verify-post-publish.sh +55 -4
  9. package/src/commands/init.js +66 -31
  10. package/src/commands/reissue-turn.js +16 -0
  11. package/src/commands/reject-turn.js +14 -1
  12. package/src/commands/restart.js +33 -3
  13. package/src/commands/resume.js +78 -66
  14. package/src/commands/run.js +67 -10
  15. package/src/commands/schedule.js +34 -7
  16. package/src/commands/status.js +38 -5
  17. package/src/commands/step.js +117 -34
  18. package/src/lib/adapters/api-proxy-adapter.js +8 -0
  19. package/src/lib/adapters/local-cli-adapter.js +131 -13
  20. package/src/lib/adapters/manual-adapter.js +9 -10
  21. package/src/lib/adapters/mcp-adapter.js +3 -5
  22. package/src/lib/adapters/remote-agent-adapter.js +3 -5
  23. package/src/lib/config.js +4 -1
  24. package/src/lib/continuous-run.js +71 -6
  25. package/src/lib/dashboard/actions.js +9 -3
  26. package/src/lib/dashboard/bridge-server.js +11 -0
  27. package/src/lib/dashboard/notifications-reader.js +91 -0
  28. package/src/lib/dashboard/state-reader.js +16 -4
  29. package/src/lib/dispatch-bundle.js +1 -1
  30. package/src/lib/dispatch-progress.js +5 -3
  31. package/src/lib/governed-state.js +355 -13
  32. package/src/lib/intake.js +10 -1
  33. package/src/lib/normalized-config.js +51 -1
  34. package/src/lib/recent-event-summary.js +12 -0
  35. package/src/lib/run-events.js +4 -0
  36. package/src/lib/run-loop.js +67 -2
  37. package/src/lib/runner-interface.js +1 -0
  38. package/src/lib/schema.js +7 -0
  39. package/src/lib/schemas/agentxchain-config.schema.json +15 -1
  40. package/src/lib/staged-result-proof.js +43 -0
  41. package/src/lib/stale-turn-watchdog.js +308 -34
  42. package/src/lib/turn-result-shape.js +38 -0
  43. package/src/lib/turn-result-validator.js +4 -1
package/dashboard/app.js CHANGED
@@ -15,6 +15,7 @@ import { render as renderCrossRepo } from './components/cross-repo.js';
15
15
  import { render as renderDelegations } from './components/delegations.js';
16
16
  import { render as renderBlockers } from './components/blockers.js';
17
17
  import { render as renderArtifacts } from './components/artifacts.js';
18
+ import { render as renderNotifications } from './components/notifications.js';
18
19
  import { render as renderMission } from './components/mission.js';
19
20
  import { render as renderChain } from './components/chain.js';
20
21
  import { render as renderRunHistory } from './components/run-history.js';
@@ -31,6 +32,7 @@ const VIEWS = {
31
32
  delegations: { fetch: ['state', 'history'], render: renderDelegations },
32
33
  ledger: { fetch: ['state', 'ledger', 'coordinatorState', 'coordinatorLedger', 'repoDecisionsSummary'], render: renderLedger },
33
34
  hooks: { fetch: ['audit', 'annotations', 'coordinatorAudit', 'coordinatorAnnotations'], render: renderHooks },
35
+ notifications: { fetch: ['notifications'], render: renderNotifications },
34
36
  blocked: { fetch: ['state', 'audit', 'coordinatorState', 'coordinatorAudit', 'coordinatorBlockers', 'coordinatorRepoStatusRows', 'gateActions'], render: renderBlocked },
35
37
  gate: { fetch: ['state', 'history', 'coordinatorState', 'coordinatorHistory', 'coordinatorBarriers', 'gateActions'], render: renderGate },
36
38
  initiative: { fetch: ['coordinatorState', 'coordinatorBarriers', 'barrierLedger', 'coordinatorBlockers', 'coordinatorRepoStatusRows'], render: renderInitiative },
@@ -62,6 +64,7 @@ const API_MAP = {
62
64
  coordinatorBlockers: '/api/coordinator/blockers',
63
65
  coordinatorRepoStatusRows: '/api/coordinator/repo-status',
64
66
  workflowKitArtifacts: '/api/workflow-kit-artifacts',
67
+ notifications: '/api/notifications',
65
68
  missions: '/api/missions',
66
69
  plans: '/api/plans',
67
70
  chainReports: '/api/chain-reports',
@@ -0,0 +1,127 @@
1
+ function esc(str) {
2
+ if (str == null) return '';
3
+ return String(str)
4
+ .replace(/&/g, '&')
5
+ .replace(/</g, '&lt;')
6
+ .replace(/>/g, '&gt;')
7
+ .replace(/"/g, '&quot;')
8
+ .replace(/'/g, '&#39;');
9
+ }
10
+
11
+ function badge(label, color = 'var(--text-dim)') {
12
+ return `<span class="badge" style="color:${color};border-color:${color}">${esc(label)}</span>`;
13
+ }
14
+
15
+ function formatResult(entry) {
16
+ if (entry?.delivered) return badge('delivered', 'var(--green)');
17
+ if (entry?.timed_out) return badge('timed out', 'var(--yellow)');
18
+ return badge('failed', 'var(--red)');
19
+ }
20
+
21
+ function renderWebhookRow(webhook) {
22
+ return `<tr>
23
+ <td class="mono">${esc(webhook.name)}</td>
24
+ <td>${esc(webhook.timeout_ms)}</td>
25
+ <td>${esc(webhook.event_count)}</td>
26
+ <td><span class="mono">${esc((webhook.events || []).join(', '))}</span></td>
27
+ </tr>`;
28
+ }
29
+
30
+ function renderAuditRow(entry) {
31
+ const rowStyle = entry?.delivered ? '' : ' style="border-left:3px solid var(--red)"';
32
+ const statusCode = entry?.status_code == null ? '—' : String(entry.status_code);
33
+ const duration = entry?.duration_ms == null ? '—' : `${entry.duration_ms}ms`;
34
+ return `<tr${rowStyle}>
35
+ <td class="mono">${esc(entry?.emitted_at || '—')}</td>
36
+ <td><span class="mono">${esc(entry?.event_type || '—')}</span></td>
37
+ <td class="mono">${esc(entry?.notification_name || '—')}</td>
38
+ <td>${formatResult(entry)}</td>
39
+ <td>${esc(statusCode)}</td>
40
+ <td>${esc(duration)}</td>
41
+ <td>${esc(entry?.message || '—')}</td>
42
+ </tr>`;
43
+ }
44
+
45
+ export function render({ notifications }) {
46
+ if (!notifications) {
47
+ return `<div class="placeholder"><h2>Notifications</h2><p>No notification data available.</p></div>`;
48
+ }
49
+
50
+ if (notifications.ok === false) {
51
+ const hint = notifications.code === 'config_missing'
52
+ ? ' Run <code>agentxchain init --governed</code> to get started.'
53
+ : '';
54
+ return `<div class="placeholder"><h2>Notifications</h2><p>${esc(notifications.error || 'Failed to load notification data.')}${hint}</p></div>`;
55
+ }
56
+
57
+ const recent = Array.isArray(notifications.recent) ? notifications.recent : [];
58
+ const webhooks = Array.isArray(notifications.webhooks) ? notifications.webhooks : [];
59
+ const summary = notifications.summary || {};
60
+
61
+ if (!notifications.configured && recent.length === 0) {
62
+ return `<div class="placeholder"><h2>Notifications</h2><p>No <code>notifications.webhooks</code> are configured and no delivery audit entries exist yet.</p></div>`;
63
+ }
64
+
65
+ let html = `<div class="notifications-view"><div class="run-header"><div class="run-meta">`;
66
+ html += notifications.configured
67
+ ? badge(`${webhooks.length} webhook${webhooks.length === 1 ? '' : 's'} configured`, 'var(--green)')
68
+ : badge('not currently configured', 'var(--yellow)');
69
+ html += badge(`${summary.total_attempts || 0} attempts`, 'var(--accent)');
70
+ if ((summary.failed || 0) > 0) {
71
+ html += badge(`${summary.failed} failed`, 'var(--red)');
72
+ }
73
+ if ((summary.timed_out || 0) > 0) {
74
+ html += badge(`${summary.timed_out} timed out`, 'var(--yellow)');
75
+ }
76
+ if (notifications.approval_sla?.enabled) {
77
+ html += badge(`approval SLA: ${(notifications.approval_sla.reminder_after_seconds || []).join(', ')}s`, 'var(--accent)');
78
+ }
79
+ html += `</div></div>`;
80
+
81
+ if (webhooks.length > 0) {
82
+ html += `<div class="section"><h3>Notification Targets</h3>
83
+ <table class="data-table">
84
+ <thead>
85
+ <tr>
86
+ <th>Name</th>
87
+ <th>Timeout</th>
88
+ <th>Events</th>
89
+ <th>Subscribed Event Types</th>
90
+ </tr>
91
+ </thead>
92
+ <tbody>${webhooks.map(renderWebhookRow).join('')}</tbody>
93
+ </table>
94
+ </div>`;
95
+ }
96
+
97
+ html += `<div class="section"><h3>Delivery Summary</h3>
98
+ <p><strong>Delivered:</strong> ${esc(summary.delivered || 0)}<br>
99
+ <strong>Failed:</strong> ${esc(summary.failed || 0)}<br>
100
+ <strong>Last emitted:</strong> ${esc(summary.last_emitted_at || '—')}<br>
101
+ <strong>Last failure:</strong> ${esc(summary.last_failure_at || '—')}</p>
102
+ </div>`;
103
+
104
+ if (recent.length === 0) {
105
+ html += `<div class="section"><h3>Recent Delivery Attempts</h3><p style="color:var(--text-dim)">No notification deliveries recorded yet.</p></div>`;
106
+ } else {
107
+ html += `<div class="section"><h3>Recent Delivery Attempts</h3>
108
+ <table class="data-table">
109
+ <thead>
110
+ <tr>
111
+ <th>Emitted</th>
112
+ <th>Event</th>
113
+ <th>Target</th>
114
+ <th>Result</th>
115
+ <th>Status</th>
116
+ <th>Duration</th>
117
+ <th>Message</th>
118
+ </tr>
119
+ </thead>
120
+ <tbody>${recent.map(renderAuditRow).join('')}</tbody>
121
+ </table>
122
+ </div>`;
123
+ }
124
+
125
+ html += `</div>`;
126
+ return html;
127
+ }
@@ -401,6 +401,7 @@
401
401
  <a href="#delegations">Delegations</a>
402
402
  <a href="#ledger">Decisions</a>
403
403
  <a href="#hooks">Hooks</a>
404
+ <a href="#notifications">Notifications</a>
404
405
  <a href="#blocked">Blocked</a>
405
406
  <a href="#gate">Gates</a>
406
407
  <a href="#blockers">Blockers</a>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentxchain",
3
- "version": "2.145.0",
3
+ "version": "2.147.0",
4
4
  "description": "CLI for AgentXchain — governed multi-agent software delivery",
5
5
  "type": "module",
6
6
  "bin": {
@@ -84,6 +84,22 @@ NEW_VERSION="$(node -e "console.log(JSON.parse(require('fs').readFileSync('packa
84
84
  echo "New version: ${NEW_VERSION}"
85
85
  echo ""
86
86
 
87
+ # Publish-gate enforcement: WAYS-OF-WORKING section 9 prohibits bypassing the
88
+ # publish gate with manual npm publish. Even though this script is a documented
89
+ # non-canonical helper (see RELEASE_CUT_SPEC.md section 6), it must not drop
90
+ # below the same release-boundary proof surface (claim-reality-preflight,
91
+ # beta-tester scenarios, release-docs-content, release-preflight) that the
92
+ # canonical publish-npm-on-tag.yml workflow runs via
93
+ # `release-preflight.sh --publish-gate`. Set ALLOW_PUBLISH_GATE_BYPASS=1 only
94
+ # for dry-run/debug paths that have manually established proof (for example,
95
+ # a release-preflight.sh --publish-gate run the operator just watched pass).
96
+ if [[ "${ALLOW_PUBLISH_GATE_BYPASS:-0}" != "1" ]]; then
97
+ echo "Running release-preflight.sh --publish-gate before npm publish..."
98
+ bash scripts/release-preflight.sh --publish-gate --target-version "${NEW_VERSION}"
99
+ else
100
+ echo "WARNING: ALLOW_PUBLISH_GATE_BYPASS=1 set — skipping publish-gate. The operator owns claim-reality/beta-tester proof manually."
101
+ fi
102
+
87
103
  echo "Publishing to npm..."
88
104
  if [[ -n "${NPM_TOKEN:-}" ]]; then
89
105
  npm publish --access public --//registry.npmjs.org/:_authToken="${NPM_TOKEN}"
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env bash
2
2
  # Release downstream truth — run after all downstream surfaces are updated.
3
- # Verifies: GitHub release exists, Homebrew tap SHA and URL match registry tarball.
3
+ # Verifies: GitHub release is published on the expected tag URL, Homebrew tap SHA and URL match registry tarball.
4
4
  # Usage: bash scripts/release-downstream-truth.sh --target-version <semver>
5
5
  set -uo pipefail
6
6
 
@@ -91,22 +91,30 @@ echo "[1/3] GitHub release"
91
91
  if ! command -v gh >/dev/null 2>&1; then
92
92
  fail "gh CLI not available — cannot verify GitHub release"
93
93
  else
94
- GH_FOUND=false
94
+ GH_READY=false
95
+ EXPECTED_GH_URL="https://github.com/shivamtiwari93/agentXchain.dev/releases/tag/v${TARGET_VERSION}"
95
96
  for attempt in $(seq 1 "$RETRY_ATTEMPTS"); do
96
97
  GH_TAG="$(gh release view "v${TARGET_VERSION}" --json tagName -q '.tagName' 2>/dev/null || true)"
97
- if [[ "$GH_TAG" == "v${TARGET_VERSION}" ]]; then
98
- GH_FOUND=true
98
+ GH_DRAFT="$(gh release view "v${TARGET_VERSION}" --json isDraft -q '.isDraft' 2>/dev/null || true)"
99
+ GH_URL="$(gh release view "v${TARGET_VERSION}" --json url -q '.url' 2>/dev/null || true)"
100
+ GH_PUBLISHED_AT="$(gh release view "v${TARGET_VERSION}" --json publishedAt -q '.publishedAt' 2>/dev/null || true)"
101
+ if [[ "$GH_TAG" == "v${TARGET_VERSION}" ]] \
102
+ && [[ "$GH_DRAFT" == "false" ]] \
103
+ && [[ "$GH_URL" == "$EXPECTED_GH_URL" ]] \
104
+ && [[ -n "$GH_PUBLISHED_AT" ]] \
105
+ && [[ "$GH_PUBLISHED_AT" != "null" ]]; then
106
+ GH_READY=true
99
107
  break
100
108
  fi
101
109
  if [[ "$attempt" -lt "$RETRY_ATTEMPTS" ]]; then
102
- echo " INFO: GitHub release not found (attempt ${attempt}/${RETRY_ATTEMPTS}); retrying in ${RETRY_DELAY_SECONDS}s..."
110
+ echo " INFO: GitHub release not ready (attempt ${attempt}/${RETRY_ATTEMPTS}); retrying in ${RETRY_DELAY_SECONDS}s..."
103
111
  sleep "$RETRY_DELAY_SECONDS"
104
112
  fi
105
113
  done
106
- if $GH_FOUND; then
107
- pass "GitHub release v${TARGET_VERSION} exists"
114
+ if $GH_READY; then
115
+ pass "GitHub release v${TARGET_VERSION} is published on the tagged release URL"
108
116
  else
109
- fail "GitHub release v${TARGET_VERSION} not found after ${RETRY_ATTEMPTS} attempts"
117
+ fail "GitHub release v${TARGET_VERSION} is not fully published (tag=${GH_TAG:-<missing>} draft=${GH_DRAFT:-<missing>} url=${GH_URL:-<missing>} publishedAt=${GH_PUBLISHED_AT:-<missing>})"
110
118
  fi
111
119
  fi
112
120
 
@@ -244,5 +244,18 @@ fi
244
244
 
245
245
  echo ""
246
246
  echo "====================================="
247
- echo "SYNC COMPLETE — Homebrew formula updated to ${PACKAGE_NAME}@${TARGET_VERSION}."
247
+ echo "SYNC STEP COMPLETE — Homebrew formula updated to ${PACKAGE_NAME}@${TARGET_VERSION}."
248
+ echo ""
249
+ echo "This is the Phase 2 -> Phase 3 transition step only. It does NOT prove"
250
+ echo "the public npx install path resolves, and it does NOT prove the canonical"
251
+ echo "tap / GitHub Release / repo-mirror downstream truth is consistent."
252
+ echo ""
253
+ echo "Do NOT declare the release complete from this script's exit code alone."
254
+ echo "Complete the release by running ONE of:"
255
+ echo " - bash cli/scripts/verify-post-publish.sh --target-version ${TARGET_VERSION}"
256
+ echo " (manual/operator path; includes npx smoke + repo-mirror SHA proof + full test suite)"
257
+ echo " - bash cli/scripts/release-downstream-truth.sh --target-version ${TARGET_VERSION}"
258
+ echo " (CI-equivalent path; requires release-postflight.sh to have already run the npx smoke)"
259
+ echo ""
260
+ echo "See DEC-VERIFY-POST-PUBLISH-NPX-001 and DEC-HOMEBREW-SYNC-LOOPHOLE-CLOSE-001."
248
261
  exit 0
@@ -20,6 +20,8 @@ cd "$CLI_DIR"
20
20
  TARGET_VERSION=""
21
21
 
22
22
  FORMULA_PATH="${CLI_DIR}/homebrew/agentxchain.rb"
23
+ PACKAGE_NAME="$(node -e "console.log(JSON.parse(require('fs').readFileSync('package.json','utf8')).name)")"
24
+ PACKAGE_BIN_NAME="$(node -e "const pkg = JSON.parse(require('fs').readFileSync('package.json','utf8')); if (typeof pkg.bin === 'string') { console.log(pkg.name); process.exit(0); } const names = Object.keys(pkg.bin || {}); if (names.length !== 1) { console.error('package.json bin must declare exactly one entry'); process.exit(1); } console.log(names[0]);")"
23
25
 
24
26
  formula_url() {
25
27
  local formula_path="$1"
@@ -31,6 +33,29 @@ formula_sha() {
31
33
  grep -E '^\s*sha256\s+"' "$formula_path" | sed 's/.*sha256 *"\([a-f0-9]*\)".*/\1/' || true
32
34
  }
33
35
 
36
+ run_npx_smoke() {
37
+ local smoke_root
38
+ local smoke_npmrc
39
+
40
+ smoke_root="$(mktemp -d "${TMPDIR:-/tmp}/agentxchain-verify-post-publish.XXXXXX")"
41
+ mkdir -p "${smoke_root}/home" "${smoke_root}/cache" "${smoke_root}/npm-cache" "${smoke_root}/workspace"
42
+ smoke_npmrc="${smoke_root}/.npmrc"
43
+ echo "registry=https://registry.npmjs.org/" > "$smoke_npmrc"
44
+
45
+ (
46
+ cd "${smoke_root}/workspace" || exit 1
47
+ env -u NODE_AUTH_TOKEN \
48
+ HOME="${smoke_root}/home" \
49
+ XDG_CACHE_HOME="${smoke_root}/cache" \
50
+ NPM_CONFIG_CACHE="${smoke_root}/npm-cache" \
51
+ NPM_CONFIG_USERCONFIG="${smoke_npmrc}" \
52
+ npx --yes -p "${PACKAGE_NAME}@${TARGET_VERSION}" -c "${PACKAGE_BIN_NAME} --version"
53
+ )
54
+ local status=$?
55
+ rm -rf "$smoke_root"
56
+ return "$status"
57
+ }
58
+
34
59
  while [[ $# -gt 0 ]]; do
35
60
  case "$1" in
36
61
  --target-version)
@@ -57,7 +82,7 @@ echo "============================================="
57
82
  echo ""
58
83
 
59
84
  # Step 1: Verify npm serves the version
60
- echo "[1/4] Checking npm registry..."
85
+ echo "[1/5] Checking npm registry..."
61
86
  NPM_VERSION="$(npm view "agentxchain@${TARGET_VERSION}" version 2>/dev/null || echo "")"
62
87
  if [[ "$NPM_VERSION" != "$TARGET_VERSION" ]]; then
63
88
  echo " FAIL: npm does not serve agentxchain@${TARGET_VERSION} (got: '${NPM_VERSION}')"
@@ -67,7 +92,7 @@ fi
67
92
  echo " OK: npm serves v${TARGET_VERSION}"
68
93
 
69
94
  # Step 2: Sync the repo mirror to the published tarball
70
- echo "[2/4] Syncing repo mirror to published tarball..."
95
+ echo "[2/5] Syncing repo mirror to published tarball..."
71
96
  bash "${SCRIPT_DIR}/sync-homebrew.sh" --target-version "$TARGET_VERSION"
72
97
  echo " OK: repo mirror synced"
73
98
 
@@ -109,8 +134,34 @@ if [[ "$FORMULA_SHA" != "$TARBALL_SHA" ]]; then
109
134
  fi
110
135
  echo " OK: repo mirror formula SHA256 matches registry tarball"
111
136
 
112
- # Step 4: Run the full test suite WITHOUT the preflight skip
113
- echo "[4/5] Running full test suite (no preflight skip)..."
137
+ # Step 4: Recheck the public npx path against the published registry version
138
+ echo "[4/5] Verifying public npx install path..."
139
+ NPX_OUTPUT="$(run_npx_smoke 2>&1 || true)"
140
+ NPX_VERSION="$(printf '%s\n' "$NPX_OUTPUT" | awk -v expected="${TARGET_VERSION}" '
141
+ {
142
+ line=$0
143
+ gsub(/^[[:space:]]+|[[:space:]]+$/, "", line)
144
+ if (line == expected) {
145
+ print line
146
+ found=1
147
+ exit
148
+ }
149
+ }
150
+ END {
151
+ if (!found) {
152
+ exit 1
153
+ }
154
+ }
155
+ ' || true)"
156
+ if [[ "$NPX_VERSION" != "$TARGET_VERSION" ]]; then
157
+ echo " FAIL: public npx path did not report ${TARGET_VERSION}"
158
+ printf '%s\n' "$NPX_OUTPUT"
159
+ exit 1
160
+ fi
161
+ echo " OK: public npx path resolves and reports v${TARGET_VERSION}"
162
+
163
+ # Step 5: Run the full test suite WITHOUT the preflight skip
164
+ echo "[5/5] Running full test suite (no preflight skip)..."
114
165
  echo " This verifies the broader Homebrew mirror contract passes with the real SHA."
115
166
  npm test
116
167
  echo " OK: full test suite green"
@@ -103,6 +103,68 @@ const DEFAULT_GOVERNED_LOCAL_DEV_RUNTIME = Object.freeze({
103
103
  prompt_transport: 'stdin',
104
104
  });
105
105
 
106
+ const GOVERNED_GITIGNORE_LINES = Object.freeze([
107
+ '.env',
108
+ '.agentxchain/staging/',
109
+ '.agentxchain/dispatch/',
110
+ '.agentxchain/transactions/',
111
+ '.agentxchain/state.json',
112
+ '.agentxchain/session.json',
113
+ '.agentxchain/history.jsonl',
114
+ '.agentxchain/decision-ledger.jsonl',
115
+ '.agentxchain/repo-decisions.jsonl',
116
+ '.agentxchain/lock.json',
117
+ '.agentxchain/hook-audit.jsonl',
118
+ '.agentxchain/hook-annotations.jsonl',
119
+ '.agentxchain/run-history.jsonl',
120
+ '.agentxchain/events.jsonl',
121
+ '.agentxchain/notification-audit.jsonl',
122
+ '.agentxchain/schedule-state.json',
123
+ '.agentxchain/schedule-daemon.json',
124
+ '.agentxchain/continuous-session.json',
125
+ '.agentxchain/human-escalations.jsonl',
126
+ '.agentxchain/sla-reminders.json',
127
+ '.agentxchain/SESSION_RECOVERY.md',
128
+ '.agentxchain/migration-report.md',
129
+ '.agentxchain/intake/',
130
+ '.agentxchain/missions/',
131
+ '.agentxchain/multirepo/',
132
+ '.agentxchain/reviews/',
133
+ '.agentxchain/reports/',
134
+ '.agentxchain/proposed/',
135
+ 'TALK.md',
136
+ 'HUMAN_TASKS.md',
137
+ ]);
138
+
139
+ const GOVERNED_GITIGNORE_CONTENT = [
140
+ '# AgentXchain — secrets',
141
+ '.env',
142
+ '',
143
+ '# AgentXchain — transient execution artifacts (never commit)',
144
+ '.agentxchain/staging/',
145
+ '.agentxchain/dispatch/',
146
+ '.agentxchain/transactions/',
147
+ '',
148
+ '# AgentXchain — framework-owned state (gitignored by default in fresh scaffolds)',
149
+ '# These files remain durable on disk and in export/restore, but defaulting them',
150
+ '# out of raw `git status` reduces operator noise. Existing tracked copies still',
151
+ '# appear dirty until the repo explicitly untracks them.',
152
+ ...GOVERNED_GITIGNORE_LINES.slice(4),
153
+ ].join('\n') + '\n';
154
+
155
+ function ensureGitignoreEntries(gitignorePath, content, requiredEntries) {
156
+ if (!existsSync(gitignorePath)) {
157
+ writeFileSync(gitignorePath, content);
158
+ return;
159
+ }
160
+ const existingIgnore = readFileSync(gitignorePath, 'utf8');
161
+ const existingLines = new Set(existingIgnore.split(/\r?\n/));
162
+ const missing = requiredEntries.filter(entry => !existingLines.has(entry));
163
+ if (missing.length === 0) return;
164
+ const prefix = existingIgnore.endsWith('\n') || existingIgnore.length === 0 ? '' : '\n';
165
+ writeFileSync(gitignorePath, existingIgnore + prefix + missing.join('\n') + '\n');
166
+ }
167
+
106
168
  const GOVERNED_RUNTIMES = {
107
169
  'manual-pm': { type: 'manual' },
108
170
  'manual-dev': { type: 'manual' },
@@ -833,28 +895,10 @@ export function scaffoldGoverned(dir, projectName, projectId, templateId = 'gene
833
895
  // TALK.md
834
896
  writeFileSync(join(dir, 'TALK.md'), `# ${projectName} — Team Talk File\n\nCanonical human-readable handoff log for all agents.\n\n---\n\n`);
835
897
 
836
- // .gitignore additions with inline comments so operators know what to commit vs. ignore
898
+ // .gitignore additions with inline comments so fresh governed repos keep
899
+ // framework-owned runtime state out of raw git status by default.
837
900
  const gitignorePath = join(dir, '.gitignore');
838
- const gitignoreContent = [
839
- '# AgentXchain — secrets',
840
- '.env',
841
- '',
842
- '# AgentXchain — transient execution artifacts (never commit)',
843
- '.agentxchain/staging/',
844
- '.agentxchain/dispatch/',
845
- '.agentxchain/transactions/',
846
- ].join('\n') + '\n';
847
- const requiredPaths = ['.env', '.agentxchain/staging/', '.agentxchain/dispatch/', '.agentxchain/transactions/'];
848
- if (!existsSync(gitignorePath)) {
849
- writeFileSync(gitignorePath, gitignoreContent);
850
- } else {
851
- const existingIgnore = readFileSync(gitignorePath, 'utf8');
852
- const missing = requiredPaths.filter(entry => !existingIgnore.split(/\r?\n/).includes(entry));
853
- if (missing.length > 0) {
854
- const prefix = existingIgnore.endsWith('\n') ? '' : '\n';
855
- writeFileSync(gitignorePath, existingIgnore + prefix + missing.join('\n') + '\n');
856
- }
857
- }
901
+ ensureGitignoreEntries(gitignorePath, GOVERNED_GITIGNORE_CONTENT, GOVERNED_GITIGNORE_LINES);
858
902
 
859
903
  return { config, state, scaffoldWorkflowKitConfig };
860
904
  }
@@ -1251,16 +1295,7 @@ export async function initCommand(opts) {
1251
1295
  writeFileSync(join(dir, 'HUMAN_TASKS.md'), '# Human Tasks\n\n(Agents append tasks here when they need human action.)\n');
1252
1296
  const gitignorePath = join(dir, '.gitignore');
1253
1297
  const requiredIgnores = ['.env', '.agentxchain-trigger.json', '.agentxchain-prompts/', '.agentxchain-workspaces/', '.agentxchain-watch.pid', '.agentxchain-autonudge.state'];
1254
- if (!existsSync(gitignorePath)) {
1255
- writeFileSync(gitignorePath, requiredIgnores.join('\n') + '\n');
1256
- } else {
1257
- const existingIgnore = readFileSync(gitignorePath, 'utf8');
1258
- const missing = requiredIgnores.filter(entry => !existingIgnore.split(/\r?\n/).includes(entry));
1259
- if (missing.length > 0) {
1260
- const prefix = existingIgnore.endsWith('\n') ? '' : '\n';
1261
- writeFileSync(gitignorePath, existingIgnore + prefix + missing.join('\n') + '\n');
1262
- }
1263
- }
1298
+ ensureGitignoreEntries(gitignorePath, requiredIgnores.join('\n') + '\n', requiredIgnores);
1264
1299
 
1265
1300
  // .planning/ structure
1266
1301
  mkdirSync(join(dir, '.planning', 'research'), { recursive: true });
@@ -18,8 +18,10 @@ import {
18
18
  getActiveTurns,
19
19
  getActiveTurn,
20
20
  reissueTurn,
21
+ transitionActiveTurnLifecycle,
21
22
  } from '../lib/governed-state.js';
22
23
  import { writeDispatchBundle } from '../lib/dispatch-bundle.js';
24
+ import { finalizeDispatchManifest } from '../lib/dispatch-manifest.js';
23
25
 
24
26
  export async function reissueTurnCommand(opts) {
25
27
  const context = loadProjectContext();
@@ -91,6 +93,20 @@ export async function reissueTurnCommand(opts) {
91
93
  console.log(chalk.red(`Turn reissued but dispatch bundle failed: ${bundleResult.error}`));
92
94
  process.exit(1);
93
95
  }
96
+ // BUG-51 follow-up: every command that writes a dispatch bundle for an
97
+ // active turn must finalize the manifest so adapter-side
98
+ // `verifyDispatchManifestForAdapter` enforcement matches fresh dispatches.
99
+ // Reissue does not run after_dispatch hooks, so finalization happens
100
+ // immediately after writeDispatchBundle.
101
+ const manifestResult = finalizeDispatchManifest(root, result.newTurn.turn_id, {
102
+ run_id: result.state.run_id,
103
+ role: result.newTurn.assigned_role,
104
+ });
105
+ if (!manifestResult.ok) {
106
+ console.log(chalk.red(`Turn reissued but dispatch manifest failed: ${manifestResult.error}`));
107
+ process.exit(1);
108
+ }
109
+ transitionActiveTurnLifecycle(root, result.newTurn.turn_id, 'dispatched');
94
110
 
95
111
  // Print summary
96
112
  console.log('');
@@ -2,9 +2,10 @@ import chalk from 'chalk';
2
2
  import { existsSync } from 'fs';
3
3
  import { join } from 'path';
4
4
  import { loadProjectContext, loadProjectState } from '../lib/config.js';
5
- import { getActiveTurns, rejectGovernedTurn } from '../lib/governed-state.js';
5
+ import { getActiveTurns, rejectGovernedTurn, transitionActiveTurnLifecycle } from '../lib/governed-state.js';
6
6
  import { validateStagedTurnResult } from '../lib/turn-result-validator.js';
7
7
  import { writeDispatchBundle } from '../lib/dispatch-bundle.js';
8
+ import { finalizeDispatchManifest } from '../lib/dispatch-manifest.js';
8
9
  import { deriveRecoveryDescriptor } from '../lib/blocked-state.js';
9
10
  import { getDispatchTurnDir, getTurnStagingResultPath } from '../lib/turn-paths.js';
10
11
 
@@ -51,6 +52,18 @@ export async function rejectTurnCommand(opts) {
51
52
  console.log(chalk.red(`Turn rejected but dispatch bundle rewrite failed: ${bundleResult.error}`));
52
53
  process.exit(1);
53
54
  }
55
+ // BUG-51 follow-up: finalize the manifest so adapter verification matches
56
+ // fresh dispatches. reject-for-retry does not run after_dispatch hooks,
57
+ // so finalize immediately after writeDispatchBundle.
58
+ const manifestResult = finalizeDispatchManifest(root, validation.turn.turn_id, {
59
+ run_id: result.state.run_id,
60
+ role: validation.turn.assigned_role,
61
+ });
62
+ if (!manifestResult.ok) {
63
+ console.log(chalk.red(`Turn rejected but dispatch manifest failed: ${manifestResult.error}`));
64
+ process.exit(1);
65
+ }
66
+ transitionActiveTurnLifecycle(root, validation.turn.turn_id, 'dispatched');
54
67
  printDispatchBundleWarnings(bundleResult);
55
68
  }
56
69
 
@@ -13,21 +13,25 @@
13
13
  import chalk from 'chalk';
14
14
  import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'fs';
15
15
  import { join, dirname } from 'path';
16
- import { loadProjectContext } from '../lib/config.js';
16
+ import { loadProjectContext, loadProjectState } from '../lib/config.js';
17
17
  import {
18
18
  assignGovernedTurn,
19
19
  getActiveTurns,
20
20
  getActiveTurnCount,
21
21
  reactivateGovernedRun,
22
22
  detectStateBundleDesync,
23
+ normalizeGovernedStateShape,
24
+ reconcileApprovalPausesWithConfig,
25
+ reconcileRecoveryActionsWithConfig,
26
+ transitionActiveTurnLifecycle,
23
27
  STATE_PATH,
24
28
  HISTORY_PATH,
25
29
  LEDGER_PATH,
26
30
  } from '../lib/governed-state.js';
27
31
  import { writeDispatchBundle } from '../lib/dispatch-bundle.js';
32
+ import { finalizeDispatchManifest } from '../lib/dispatch-manifest.js';
28
33
  import { getDispatchTurnDir } from '../lib/turn-paths.js';
29
34
  import { consumeNextApprovedIntent } from '../lib/intake.js';
30
- import { loadProjectState } from '../lib/config.js';
31
35
  import { deriveRecoveryDescriptor } from '../lib/blocked-state.js';
32
36
  import { deriveRecommendedContinuityAction } from '../lib/continuity-status.js';
33
37
  import { readSessionCheckpoint, writeSessionCheckpoint, captureBaselineRef, SESSION_PATH } from '../lib/session-checkpoint.js';
@@ -178,7 +182,20 @@ export async function restartCommand(opts) {
178
182
  process.exit(1);
179
183
  }
180
184
 
181
- const state = JSON.parse(readFileSync(statePath, 'utf8'));
185
+ let state;
186
+ try {
187
+ const parsed = JSON.parse(readFileSync(statePath, 'utf8'));
188
+ const normalized = normalizeGovernedStateShape(parsed);
189
+ const reconciledApprovals = reconcileApprovalPausesWithConfig(normalized.state, config);
190
+ const reconciledRecovery = reconcileRecoveryActionsWithConfig(reconciledApprovals.state, config);
191
+ state = reconciledRecovery.state;
192
+ if (normalized.changed || reconciledApprovals.changed || reconciledRecovery.changed) {
193
+ writeFileSync(statePath, JSON.stringify(state, null, 2));
194
+ }
195
+ } catch {
196
+ console.log(chalk.red('No valid governed state.json found.'));
197
+ process.exit(1);
198
+ }
182
199
 
183
200
  // Load checkpoint (optional — restart can work without it, just with less context)
184
201
  const checkpoint = readSessionCheckpoint(root);
@@ -389,6 +406,19 @@ export async function restartCommand(opts) {
389
406
  console.log(chalk.dim('Run `agentxchain reissue-turn` to reissue with a fresh bundle.'));
390
407
  process.exit(1);
391
408
  }
409
+ // BUG-51 follow-up: finalize the dispatch manifest so adapter-side
410
+ // verification matches fresh dispatches via `run`/`step`/`resume`.
411
+ // restart does not run after_dispatch hooks here, so finalize
412
+ // immediately after writeDispatchBundle.
413
+ const manifestResult = finalizeDispatchManifest(root, turnId, {
414
+ run_id: assignedState.run_id,
415
+ role: assignedRole,
416
+ });
417
+ if (!manifestResult.ok) {
418
+ console.log(chalk.red(`Turn assigned but dispatch manifest failed: ${manifestResult.error}`));
419
+ process.exit(1);
420
+ }
421
+ transitionActiveTurnLifecycle(root, turnId, 'dispatched');
392
422
  for (const bw of bundleResult.warnings || []) {
393
423
  console.log(chalk.yellow(`Dispatch bundle warning: ${bw}`));
394
424
  }