agentxchain 2.144.0 → 2.146.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/dashboard/app.js +3 -0
- package/dashboard/components/notifications.js +127 -0
- package/dashboard/index.html +1 -0
- package/package.json +1 -1
- package/scripts/release-bump.sh +82 -29
- package/scripts/release-downstream-truth.sh +16 -8
- package/src/commands/init.js +66 -31
- package/src/commands/restart.js +18 -3
- package/src/commands/resume.js +38 -0
- package/src/commands/status.js +37 -3
- package/src/commands/step.js +38 -0
- package/src/lib/config.js +4 -1
- package/src/lib/dashboard/actions.js +9 -3
- package/src/lib/dashboard/bridge-server.js +11 -0
- package/src/lib/dashboard/notifications-reader.js +91 -0
- package/src/lib/dashboard/state-reader.js +16 -4
- package/src/lib/governed-state.js +160 -0
- package/src/lib/intake.js +47 -0
- package/src/lib/intent-startup-migration.js +23 -1
- package/src/lib/recent-event-summary.js +2 -0
- package/src/lib/run-events.js +1 -0
- package/src/lib/run-history.js +23 -2
- package/src/lib/run-loop.js +3 -2
- package/src/lib/stale-turn-watchdog.js +380 -0
- package/src/lib/turn-checkpoint.js +4 -0
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, '<')
|
|
6
|
+
.replace(/>/g, '>')
|
|
7
|
+
.replace(/"/g, '"')
|
|
8
|
+
.replace(/'/g, ''');
|
|
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
|
+
}
|
package/dashboard/index.html
CHANGED
|
@@ -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
package/scripts/release-bump.sh
CHANGED
|
@@ -69,6 +69,8 @@ echo "AgentXchain Release Identity: ${TARGET_VERSION}"
|
|
|
69
69
|
echo "============================================="
|
|
70
70
|
|
|
71
71
|
TARGET_RELEASE_DOC="website-v2/docs/releases/v${TARGET_VERSION//./-}.mdx"
|
|
72
|
+
CURRENT_VERSION=$(node -e "console.log(JSON.parse(require('fs').readFileSync('package.json','utf8')).version)")
|
|
73
|
+
REENTRY_MODE=0
|
|
72
74
|
ALLOWED_RELEASE_PATHS=(
|
|
73
75
|
"cli/CHANGELOG.md"
|
|
74
76
|
"${TARGET_RELEASE_DOC}"
|
|
@@ -89,6 +91,10 @@ ALLOWED_RELEASE_PATHS=(
|
|
|
89
91
|
"cli/homebrew/agentxchain.rb"
|
|
90
92
|
"cli/homebrew/README.md"
|
|
91
93
|
)
|
|
94
|
+
ALLOWED_REENTRY_VERSION_PATHS=(
|
|
95
|
+
"cli/package.json"
|
|
96
|
+
"cli/package-lock.json"
|
|
97
|
+
)
|
|
92
98
|
|
|
93
99
|
is_allowed_release_path() {
|
|
94
100
|
local candidate="$1"
|
|
@@ -112,15 +118,32 @@ stage_if_present() {
|
|
|
112
118
|
fi
|
|
113
119
|
}
|
|
114
120
|
|
|
115
|
-
# 1.
|
|
116
|
-
echo "[1/
|
|
121
|
+
# 1. Detect version/re-entry state before validating the tree
|
|
122
|
+
echo "[1/10] Checking current version..."
|
|
123
|
+
if [[ "$CURRENT_VERSION" == "$TARGET_VERSION" ]]; then
|
|
124
|
+
REENTRY_MODE=1
|
|
125
|
+
echo " OK: package.json already targets ${TARGET_VERSION}; entering release re-entry mode"
|
|
126
|
+
else
|
|
127
|
+
echo " OK: current version is ${CURRENT_VERSION}, bumping to ${TARGET_VERSION}"
|
|
128
|
+
fi
|
|
129
|
+
|
|
130
|
+
# 2. Assert only allowed release-surface dirt is present
|
|
131
|
+
echo "[2/10] Checking release-prep tree state..."
|
|
117
132
|
DISALLOWED_DIRTY=()
|
|
118
133
|
while IFS= read -r status_line; do
|
|
119
134
|
[[ -z "$status_line" ]] && continue
|
|
120
135
|
path="${status_line#?? }"
|
|
121
|
-
if
|
|
122
|
-
|
|
136
|
+
if is_allowed_release_path "$path"; then
|
|
137
|
+
continue
|
|
138
|
+
fi
|
|
139
|
+
if [[ "$REENTRY_MODE" -eq 1 ]]; then
|
|
140
|
+
for allowed in "${ALLOWED_REENTRY_VERSION_PATHS[@]}"; do
|
|
141
|
+
if [[ "$path" == "$allowed" ]]; then
|
|
142
|
+
continue 2
|
|
143
|
+
fi
|
|
144
|
+
done
|
|
123
145
|
fi
|
|
146
|
+
DISALLOWED_DIRTY+=("$path")
|
|
124
147
|
done < <(git -C "$REPO_ROOT" status --porcelain)
|
|
125
148
|
|
|
126
149
|
if [[ "${#DISALLOWED_DIRTY[@]}" -gt 0 ]]; then
|
|
@@ -130,17 +153,8 @@ if [[ "${#DISALLOWED_DIRTY[@]}" -gt 0 ]]; then
|
|
|
130
153
|
fi
|
|
131
154
|
echo " OK: tree contains only allowed release-prep changes"
|
|
132
155
|
|
|
133
|
-
# 2. Assert not already at target version
|
|
134
|
-
echo "[2/8] Checking current version..."
|
|
135
|
-
CURRENT_VERSION=$(node -e "console.log(JSON.parse(require('fs').readFileSync('package.json','utf8')).version)")
|
|
136
|
-
if [[ "$CURRENT_VERSION" == "$TARGET_VERSION" ]]; then
|
|
137
|
-
echo "FAIL: package.json is already at ${TARGET_VERSION}. Cannot double-bump." >&2
|
|
138
|
-
exit 1
|
|
139
|
-
fi
|
|
140
|
-
echo " OK: current version is ${CURRENT_VERSION}, bumping to ${TARGET_VERSION}"
|
|
141
|
-
|
|
142
156
|
# 3. Assert tag does not already exist
|
|
143
|
-
echo "[3/
|
|
157
|
+
echo "[3/10] Checking for existing tag..."
|
|
144
158
|
if git rev-parse "v${TARGET_VERSION}" >/dev/null 2>&1; then
|
|
145
159
|
echo "FAIL: tag v${TARGET_VERSION} already exists. Delete it first or choose a different version." >&2
|
|
146
160
|
exit 1
|
|
@@ -236,7 +250,11 @@ fi
|
|
|
236
250
|
|
|
237
251
|
# 7. Update version files (no git operations)
|
|
238
252
|
echo "[7/10] Updating version files..."
|
|
239
|
-
|
|
253
|
+
if [[ "$REENTRY_MODE" -eq 1 ]]; then
|
|
254
|
+
npm version "$TARGET_VERSION" --no-git-tag-version --allow-same-version
|
|
255
|
+
else
|
|
256
|
+
npm version "$TARGET_VERSION" --no-git-tag-version
|
|
257
|
+
fi
|
|
240
258
|
echo " OK: package.json updated to ${TARGET_VERSION}"
|
|
241
259
|
|
|
242
260
|
# 8. Stage version files
|
|
@@ -251,23 +269,58 @@ done
|
|
|
251
269
|
git -C "$REPO_ROOT" add -- website-v2/docs/releases
|
|
252
270
|
echo " OK: version files and allowed release surfaces staged"
|
|
253
271
|
|
|
254
|
-
# 9. Create release commit
|
|
255
|
-
|
|
256
|
-
git
|
|
272
|
+
# 9. Create or reuse release commit
|
|
273
|
+
if git diff --cached --quiet --exit-code; then
|
|
274
|
+
CURRENT_HEAD_SHA=$(git rev-parse HEAD)
|
|
275
|
+
echo "[9/10] Resolving re-entry release identity..."
|
|
276
|
+
COMMIT_MSG=$(git log -1 --format=%s)
|
|
277
|
+
if [[ "$COMMIT_MSG" == "$TARGET_VERSION" ]]; then
|
|
278
|
+
COMMIT_BODY=$(git log -1 --format=%B)
|
|
279
|
+
if [[ "$COMMIT_BODY" != *"Co-Authored-By: ${COAUTHORED_BY}"* ]]; then
|
|
280
|
+
echo "FAIL: existing HEAD commit for re-entry is missing the required Co-Authored-By trailer" >&2
|
|
281
|
+
exit 1
|
|
282
|
+
fi
|
|
283
|
+
RELEASE_SHA=$(git rev-parse HEAD)
|
|
284
|
+
echo " OK: reusing existing release commit ${RELEASE_SHA:0:7}"
|
|
285
|
+
elif [[ "$REENTRY_MODE" -eq 1 ]]; then
|
|
286
|
+
echo " No staged release-surface deltas remain; creating metadata-only release identity commit for ${CURRENT_HEAD_SHA:0:7}"
|
|
287
|
+
git commit --allow-empty -m "${TARGET_VERSION}
|
|
288
|
+
|
|
289
|
+
Release-Base: ${CURRENT_HEAD_SHA}
|
|
290
|
+
Co-Authored-By: ${COAUTHORED_BY}"
|
|
291
|
+
RELEASE_SHA=$(git rev-parse HEAD)
|
|
292
|
+
COMMIT_BODY=$(git log -1 --format=%B)
|
|
293
|
+
if [[ "$COMMIT_BODY" != *"Release-Base: ${CURRENT_HEAD_SHA}"* ]]; then
|
|
294
|
+
echo "FAIL: metadata-only release identity commit is missing the required Release-Base line" >&2
|
|
295
|
+
exit 1
|
|
296
|
+
fi
|
|
297
|
+
if [[ "$COMMIT_BODY" != *"Co-Authored-By: ${COAUTHORED_BY}"* ]]; then
|
|
298
|
+
echo "FAIL: metadata-only release identity commit is missing the required Co-Authored-By trailer" >&2
|
|
299
|
+
exit 1
|
|
300
|
+
fi
|
|
301
|
+
echo " OK: metadata-only release identity commit ${RELEASE_SHA:0:7} recorded base ${CURRENT_HEAD_SHA:0:7}"
|
|
302
|
+
else
|
|
303
|
+
echo "FAIL: no staged release-identity changes remain, and HEAD is not already the ${TARGET_VERSION} release commit. Found commit message '${COMMIT_MSG}'." >&2
|
|
304
|
+
exit 1
|
|
305
|
+
fi
|
|
306
|
+
else
|
|
307
|
+
echo "[9/10] Creating release commit..."
|
|
308
|
+
git commit -m "${TARGET_VERSION}
|
|
257
309
|
|
|
258
310
|
Co-Authored-By: ${COAUTHORED_BY}"
|
|
259
|
-
RELEASE_SHA=$(git rev-parse HEAD)
|
|
260
|
-
COMMIT_MSG=$(git log -1 --format=%s)
|
|
261
|
-
if [[ "$COMMIT_MSG" != "$TARGET_VERSION" ]]; then
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
fi
|
|
265
|
-
COMMIT_BODY=$(git log -1 --format=%B)
|
|
266
|
-
if [[ "$COMMIT_BODY" != *"Co-Authored-By: ${COAUTHORED_BY}"* ]]; then
|
|
267
|
-
|
|
268
|
-
|
|
311
|
+
RELEASE_SHA=$(git rev-parse HEAD)
|
|
312
|
+
COMMIT_MSG=$(git log -1 --format=%s)
|
|
313
|
+
if [[ "$COMMIT_MSG" != "$TARGET_VERSION" ]]; then
|
|
314
|
+
echo "FAIL: commit message is '${COMMIT_MSG}', expected '${TARGET_VERSION}'" >&2
|
|
315
|
+
exit 1
|
|
316
|
+
fi
|
|
317
|
+
COMMIT_BODY=$(git log -1 --format=%B)
|
|
318
|
+
if [[ "$COMMIT_BODY" != *"Co-Authored-By: ${COAUTHORED_BY}"* ]]; then
|
|
319
|
+
echo "FAIL: release commit body is missing the required Co-Authored-By trailer" >&2
|
|
320
|
+
exit 1
|
|
321
|
+
fi
|
|
322
|
+
echo " OK: commit ${RELEASE_SHA:0:7} with message '${TARGET_VERSION}'"
|
|
269
323
|
fi
|
|
270
|
-
echo " OK: commit ${RELEASE_SHA:0:7} with message '${TARGET_VERSION}'"
|
|
271
324
|
|
|
272
325
|
# 9.5. Inline preflight gate — tests, pack, and docs build must pass before tag
|
|
273
326
|
if [[ "$SKIP_PREFLIGHT" -eq 1 ]]; then
|
|
@@ -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
|
|
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
|
-
|
|
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
|
-
|
|
98
|
-
|
|
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
|
|
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 $
|
|
107
|
-
pass "GitHub release v${TARGET_VERSION}
|
|
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
|
|
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
|
|
package/src/commands/init.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
-
|
|
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 });
|
package/src/commands/restart.js
CHANGED
|
@@ -13,13 +13,16 @@
|
|
|
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,
|
|
23
26
|
STATE_PATH,
|
|
24
27
|
HISTORY_PATH,
|
|
25
28
|
LEDGER_PATH,
|
|
@@ -27,7 +30,6 @@ import {
|
|
|
27
30
|
import { writeDispatchBundle } from '../lib/dispatch-bundle.js';
|
|
28
31
|
import { getDispatchTurnDir } from '../lib/turn-paths.js';
|
|
29
32
|
import { consumeNextApprovedIntent } from '../lib/intake.js';
|
|
30
|
-
import { loadProjectState } from '../lib/config.js';
|
|
31
33
|
import { deriveRecoveryDescriptor } from '../lib/blocked-state.js';
|
|
32
34
|
import { deriveRecommendedContinuityAction } from '../lib/continuity-status.js';
|
|
33
35
|
import { readSessionCheckpoint, writeSessionCheckpoint, captureBaselineRef, SESSION_PATH } from '../lib/session-checkpoint.js';
|
|
@@ -178,7 +180,20 @@ export async function restartCommand(opts) {
|
|
|
178
180
|
process.exit(1);
|
|
179
181
|
}
|
|
180
182
|
|
|
181
|
-
|
|
183
|
+
let state;
|
|
184
|
+
try {
|
|
185
|
+
const parsed = JSON.parse(readFileSync(statePath, 'utf8'));
|
|
186
|
+
const normalized = normalizeGovernedStateShape(parsed);
|
|
187
|
+
const reconciledApprovals = reconcileApprovalPausesWithConfig(normalized.state, config);
|
|
188
|
+
const reconciledRecovery = reconcileRecoveryActionsWithConfig(reconciledApprovals.state, config);
|
|
189
|
+
state = reconciledRecovery.state;
|
|
190
|
+
if (normalized.changed || reconciledApprovals.changed || reconciledRecovery.changed) {
|
|
191
|
+
writeFileSync(statePath, JSON.stringify(state, null, 2));
|
|
192
|
+
}
|
|
193
|
+
} catch {
|
|
194
|
+
console.log(chalk.red('No valid governed state.json found.'));
|
|
195
|
+
process.exit(1);
|
|
196
|
+
}
|
|
182
197
|
|
|
183
198
|
// Load checkpoint (optional — restart can work without it, just with less context)
|
|
184
199
|
const checkpoint = readSessionCheckpoint(root);
|
package/src/commands/resume.js
CHANGED
|
@@ -40,6 +40,7 @@ import { deriveRecoveryDescriptor } from '../lib/blocked-state.js';
|
|
|
40
40
|
import { runHooks } from '../lib/hook-runner.js';
|
|
41
41
|
import { summarizeRunProvenance } from '../lib/run-provenance.js';
|
|
42
42
|
import { consumeNextApprovedIntent } from '../lib/intake.js';
|
|
43
|
+
import { reconcileStaleTurns } from '../lib/stale-turn-watchdog.js';
|
|
43
44
|
|
|
44
45
|
export async function resumeCommand(opts) {
|
|
45
46
|
const context = loadProjectContext();
|
|
@@ -75,6 +76,17 @@ export async function resumeCommand(opts) {
|
|
|
75
76
|
process.exit(1);
|
|
76
77
|
}
|
|
77
78
|
|
|
79
|
+
const staleReconciliation = reconcileStaleTurns(root, state, config);
|
|
80
|
+
state = staleReconciliation.state || state;
|
|
81
|
+
if (staleReconciliation.ghost_turns.length > 0) {
|
|
82
|
+
printGhostTurnRecovery(staleReconciliation.ghost_turns);
|
|
83
|
+
process.exit(1);
|
|
84
|
+
}
|
|
85
|
+
if (staleReconciliation.stale_turns.length > 0) {
|
|
86
|
+
printStaleTurnRecovery(staleReconciliation.stale_turns);
|
|
87
|
+
process.exit(1);
|
|
88
|
+
}
|
|
89
|
+
|
|
78
90
|
// §47: active + turns present → reject (resume assigns new turns, not re-dispatches)
|
|
79
91
|
const activeCount = getActiveTurnCount(state);
|
|
80
92
|
const activeTurns = getActiveTurns(state);
|
|
@@ -351,6 +363,32 @@ export async function resumeCommand(opts) {
|
|
|
351
363
|
printDispatchSummary(state, config);
|
|
352
364
|
}
|
|
353
365
|
|
|
366
|
+
function printGhostTurnRecovery(ghostTurns) {
|
|
367
|
+
console.log(chalk.red.bold('Ghost turn detected — subprocess never started.'));
|
|
368
|
+
console.log('');
|
|
369
|
+
for (const ghost of ghostTurns) {
|
|
370
|
+
const secs = Math.floor(ghost.running_ms / 1000);
|
|
371
|
+
console.log(` Turn: ${ghost.turn_id} (${ghost.role})`);
|
|
372
|
+
console.log(` Runtime: ${ghost.runtime_id}`);
|
|
373
|
+
console.log(` Age: ${secs}s with no subprocess output`);
|
|
374
|
+
console.log(` Recover: ${chalk.cyan(`agentxchain reissue-turn --turn ${ghost.turn_id} --reason ghost`)}`);
|
|
375
|
+
console.log('');
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
function printStaleTurnRecovery(staleTurns) {
|
|
380
|
+
console.log(chalk.red.bold('Stale turn detected.'));
|
|
381
|
+
console.log('');
|
|
382
|
+
for (const stale of staleTurns) {
|
|
383
|
+
const mins = Math.floor(stale.running_ms / 60000);
|
|
384
|
+
console.log(` Turn: ${stale.turn_id} (${stale.role})`);
|
|
385
|
+
console.log(` Runtime: ${stale.runtime_id}`);
|
|
386
|
+
console.log(` Age: ${mins}m with no output`);
|
|
387
|
+
console.log(` Recover: ${chalk.cyan(`agentxchain reissue-turn --turn ${stale.turn_id} --reason stale`)}`);
|
|
388
|
+
console.log('');
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
354
392
|
// ── Helpers ─────────────────────────────────────────────────────────────────
|
|
355
393
|
|
|
356
394
|
function printResumeRunContext({ root, state, config }) {
|