session-orchestrator 5.4.0 → 5.6.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/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/.cursor/rules/060-evolve.mdc +5 -5
- package/.cursor-plugin/plugin.json +1 -1
- package/CHANGELOG.md +115 -3
- package/README.md +30 -15
- package/docs/ci-setup.md +120 -23
- package/docs/components.md +7 -7
- package/docs/events-schema.md +17 -14
- package/docs/install.md +14 -2
- package/docs/migration-v4.md +24 -17
- package/docs/plugin-architecture-v3.md +1 -1
- package/docs/scope-collision-guard.md +19 -15
- package/docs/session-config-reference.md +7 -7
- package/docs/vault-docs-architecture.md +8 -4
- package/hooks/_lib/hook-import-set.json +17 -8
- package/hooks/_lib/lock-bootstrap.mjs +118 -26
- package/hooks/config-protection.mjs +1 -1
- package/hooks/enforce-commands.mjs +28 -13
- package/hooks/enforce-scope.mjs +201 -23
- package/hooks/hooks-codex.json +1 -1
- package/hooks/hooks.json +1 -1
- package/hooks/on-session-start.mjs +4 -2
- package/hooks/on-stop.mjs +77 -171
- package/hooks/post-bash-write-verify.mjs +219 -24
- package/hooks/post-edit-validate.mjs +15 -5
- package/hooks/post-tool-batch-wave-signal.mjs +41 -156
- package/hooks/pre-bash-memory-propose-audit.mjs +28 -11
- package/hooks/pre-bash-staging-fence.mjs +29 -8
- package/hooks/pre-task-scope-disjoint.mjs +603 -241
- package/hooks/wave-scope-commit-guard.mjs +235 -73
- package/package.json +9 -8
- package/scripts/apply-session-learnings.mjs +35 -14
- package/scripts/archive-closed-prds.mjs +38 -11
- package/scripts/autopilot.mjs +8 -1
- package/scripts/backfill-abandoned-sessions.mjs +80 -5
- package/scripts/backfill-learnings-from-vault.mjs +49 -22
- package/scripts/codex-install.mjs +3 -0
- package/scripts/cursor-install.mjs +46 -2
- package/scripts/emit-session.mjs +5 -3
- package/scripts/export-hw-learnings.mjs +18 -7
- package/scripts/fleet-checkin.mjs +26 -3
- package/scripts/lib/autopilot/loop.mjs +25 -10
- package/scripts/lib/autopilot/stall-sampler.mjs +13 -9
- package/scripts/lib/blocked-commands-policy.mjs +40 -0
- package/scripts/lib/common.mjs +41 -0
- package/scripts/lib/config/vault-integration.mjs +151 -16
- package/scripts/lib/config.mjs +5 -1
- package/scripts/lib/eval/engine.mjs +13 -0
- package/scripts/lib/events-retention-banner.mjs +393 -0
- package/scripts/lib/events-rotation.mjs +21 -6
- package/scripts/lib/events.mjs +51 -3
- package/scripts/lib/fleet-protocol.mjs +20 -2
- package/scripts/lib/hardening.mjs +1 -0
- package/scripts/lib/harness-audit/categories/category4.mjs +8 -6
- package/scripts/lib/instruction-budget-guard.mjs +8 -0
- package/scripts/lib/learnings/expiry-sweep.mjs +387 -20
- package/scripts/lib/learnings/io.mjs +125 -16
- package/scripts/lib/lock-reaper.mjs +38 -14
- package/scripts/lib/maintenance-due-banner.mjs +23 -8
- package/scripts/lib/named-vault-resolver.mjs +48 -5
- package/scripts/lib/orphan-reaper/no-follow-append.mjs +87 -0
- package/scripts/lib/orphan-reaper/reaper-audit.mjs +101 -69
- package/scripts/lib/orphan-reaper/scan-throttle.mjs +79 -27
- package/scripts/lib/orphan-reaper/scan.mjs +4 -0
- package/scripts/lib/orphan-reaper/trigger.mjs +173 -0
- package/scripts/lib/orphan-reaper.mjs +5 -1
- package/scripts/lib/platform.mjs +206 -2
- package/scripts/lib/plugin-update-banner.mjs +3 -1
- package/scripts/lib/process-group.mjs +83 -22
- package/scripts/lib/scope-baseline.mjs +116 -25
- package/scripts/lib/scope-echo.mjs +7 -0
- package/scripts/lib/scope-gate.mjs +201 -27
- package/scripts/lib/session-close-backfill.mjs +202 -4
- package/scripts/lib/session-end/tail-runner.mjs +10 -5
- package/scripts/lib/session-lock-shape.mjs +6 -3
- package/scripts/lib/session-lock.mjs +320 -67
- package/scripts/lib/session-registry.mjs +41 -16
- package/scripts/lib/session-start-probes.mjs +242 -68
- package/scripts/lib/session-token-rollup.mjs +39 -34
- package/scripts/lib/sessions-staleness-banner.mjs +3 -1
- package/scripts/lib/tail-window.mjs +27 -3
- package/scripts/lib/telemetry/pricing.mjs +5 -2
- package/scripts/lib/telemetry/sync.mjs +17 -0
- package/scripts/lib/tests-src-ratio.mjs +77 -2
- package/scripts/lib/tmux-layout/telemetry-stats.mjs +12 -0
- package/scripts/lib/validate/check-test-value-bans.mjs +320 -32
- package/scripts/lib/validate/check-vcs-repo-flag.mjs +1 -1
- package/scripts/lib/vault-mirror/auto-commit.mjs +5 -3
- package/scripts/lib/vault-mirror/namespace.mjs +47 -26
- package/scripts/lib/vault-mirror/process.mjs +350 -62
- package/scripts/lib/vault-mirror/pseudonym-map.mjs +14 -5
- package/scripts/lib/vault-mirror/render-learnings.mjs +66 -3
- package/scripts/lib/vault-mirror/render-sessions.mjs +28 -4
- package/scripts/lib/vault-mirror/telemetry.mjs +18 -11
- package/scripts/lib/vault-status/board-writer.mjs +5 -1
- package/scripts/lib/vcs-repo-spec.mjs +8 -0
- package/scripts/materialize-wave-scope.mjs +7 -4
- package/scripts/release.mjs +1 -0
- package/scripts/run-migrate-v2-cross-repo.mjs +14 -2
- package/scripts/run-quality-gate.mjs +8 -5
- package/scripts/self-update.mjs +229 -0
- package/scripts/sweep-expired-learnings.mjs +560 -28
- package/scripts/validate-wave-scope.mjs +60 -6
- package/scripts/vault-backfill.mjs +50 -37
- package/scripts/vault-mirror.mjs +170 -33
- package/skills/_shared/fleet-protocol.md +74 -71
- package/skills/_shared/parallel-aware-auq.md +1 -1
- package/skills/_shared/state-ownership.md +38 -14
- package/skills/autopilot/SKILL.md +1 -1
- package/skills/bootstrap/SKILL.md +5 -3
- package/skills/bootstrap/_shared-template.md +96 -1
- package/skills/bootstrap/fast-template.md +3 -0
- package/skills/bootstrap/references/bootstrap-refresh-lock-flow.md +5 -3
- package/skills/bootstrap/standard-template.md +2 -0
- package/skills/discovery/probes/ssot-code-diff.mjs +12 -6
- package/skills/discovery/probes/vault-narrative-staleness.mjs +6 -2
- package/skills/discovery/probes/vault-staleness.mjs +9 -3
- package/skills/docs-orchestrator/SKILL.md +12 -5
- package/skills/docs-orchestrator/audience-mapping.md +5 -3
- package/skills/evolve/SKILL.md +11 -2
- package/skills/evolve/references/evolve-analyze-mode.md +37 -17
- package/skills/session-end/learning-patterns.md +3 -3
- package/skills/session-end/metrics-collection.md +1 -1
- package/skills/session-end/phase-3-2-docs-verification.md +7 -6
- package/skills/session-end/plan-verification.md +25 -9
- package/skills/session-end/references/phase-3-documentation-updates.md +11 -1
- package/skills/session-end/session-metrics-write.md +45 -10
- package/skills/session-start/SKILL.md +3 -3
- package/skills/session-start/references/operations-contract.md +5 -3
- package/skills/session-start/references/phase-1-2-session-lock.md +32 -7
- package/skills/session-start/references/phase-4-ssot-environment-check.md +8 -1
- package/skills/vault-mirror/SKILL.md +21 -11
- package/skills/wave-executor/SKILL.md +1 -1
- package/skills/wave-executor/circuit-breaker.md +1 -1
- package/skills/wave-executor/references/wave-executor-state-init.md +1 -1
- package/skills/wave-executor/references/wave-loop-dispatch.md +8 -0
- package/skills/wave-executor/references/wave-loop-review.md +2 -2
- package/skills/wave-executor/references/wave-loop-scope-manifest.md +14 -6
- package/.agents/skills/navigator/SKILL.md +0 -22
- package/.agents/skills/navigator/agents/openai.yaml +0 -5
- package/.codex-plugin/skills/navigator/SKILL.md +0 -22
- package/.codex-plugin/skills/navigator/agents/openai.yaml +0 -5
- package/.cursor/commands/navigator.md +0 -15
- package/.cursor/skills/navigator/SKILL.md +0 -13
- package/pi/prompts/navigator.md +0 -12
- package/skills/navigator/SKILL.md +0 -193
- package/skills/navigator/references/account-check.sh +0 -87
- package/skills/navigator/references/fleet-decisions.mjs +0 -231
- package/skills/navigator/references/slot-check.sh +0 -114
|
@@ -22,26 +22,49 @@
|
|
|
22
22
|
// agent's fence — i.e. another agent's intent to stage that path.
|
|
23
23
|
//
|
|
24
24
|
// Behavior summary
|
|
25
|
-
// - No
|
|
26
|
-
//
|
|
27
|
-
// -
|
|
25
|
+
// - No wave-scope.json → exit 0 (no active wave; sub-mode B short-circuits).
|
|
26
|
+
// Sub-mode C still runs if a fence directory exists.
|
|
27
|
+
// - Sub-mode B, in order (#1493.1): a manifest bound to ANOTHER session is
|
|
28
|
+
// not this commit's scope (stand down, one stderr note); `enforcement:
|
|
29
|
+
// "off"` skips.
|
|
30
|
+
// - Non-empty allowedPaths: a staged path outside it BLOCKS (exit 1) under
|
|
31
|
+
// every enforcement level, `warn` included. Deliberately not tied to
|
|
32
|
+
// `enforcement`: in a warn-mode wave enforce-scope lets out-of-scope writes
|
|
33
|
+
// through, so this guard is the only hard PSA-004 stop left for a
|
|
34
|
+
// lint-staged sweep — softening it would weaken detection for the very
|
|
35
|
+
// class it exists to catch.
|
|
36
|
+
// - EMPTY allowedPaths is no longer a silent pass: a Discovery (read-only)
|
|
37
|
+
// manifest means no agent is committing, i.e. a coordinator commit between
|
|
38
|
+
// waves → allow with a note; a manifest older than the newest session
|
|
39
|
+
// clock may be a leftover or a parallel session's live one → report,
|
|
40
|
+
// never block; any other role means the `--union` step never completed →
|
|
41
|
+
// `strict` (the default when the field is absent, as in enforce-scope)
|
|
42
|
+
// blocks, `warn` reports and lets the commit through. Only this new
|
|
43
|
+
// verdict follows `enforcement`: it judges the coordinator's own
|
|
44
|
+
// bookkeeping, not a staged foreign file.
|
|
28
45
|
// - Cross-agent fence overlap → exit 1 with "staging-fence: cross-agent
|
|
29
46
|
// overlap" stderr.
|
|
30
47
|
// - --no-verify bypass: this hook is invoked as a git pre-commit hook;
|
|
31
48
|
// `git commit --no-verify` skips it entirely (operator opts out by name).
|
|
32
49
|
|
|
33
50
|
import { execSync } from 'node:child_process';
|
|
34
|
-
import { existsSync, readFileSync, readdirSync } from 'node:fs';
|
|
35
|
-
import { join } from 'node:path';
|
|
51
|
+
import { existsSync, readFileSync, readdirSync, realpathSync, statSync } from 'node:fs';
|
|
52
|
+
import { isAbsolute, join, relative, sep } from 'node:path';
|
|
36
53
|
|
|
37
54
|
// Re-use existing helpers from scripts/lib/hardening.mjs.
|
|
38
55
|
// IMPORTANT: import is resolved relative to THIS file's location (the hook
|
|
39
56
|
// ships with the session-orchestrator package), NOT relative to the git
|
|
40
57
|
// repo it is protecting. The two diverge when the hook is invoked from a
|
|
41
58
|
// consumer repo or a test tmp-dir.
|
|
42
|
-
import { pathMatchesPattern
|
|
59
|
+
import { pathMatchesPattern } from '../scripts/lib/hardening.mjs';
|
|
43
60
|
import { withStagingFenceLock } from '../scripts/lib/session-lock.mjs';
|
|
44
61
|
import { isMainModule } from '../scripts/lib/is-main-module.mjs';
|
|
62
|
+
import { resolveSessionRoot } from '../scripts/lib/platform.mjs';
|
|
63
|
+
import { classifyEmptyScope, findOwnScopeFile, sessionAgeMs } from '../scripts/lib/scope-gate.mjs';
|
|
64
|
+
import {
|
|
65
|
+
classifyManifestSession,
|
|
66
|
+
readProcessLocalSessionIds,
|
|
67
|
+
} from '../scripts/lib/session-identity/own-session.mjs';
|
|
45
68
|
|
|
46
69
|
// Resolve the "current" agent id from SO_WAVE_AGENT_ID (when set by the
|
|
47
70
|
// caller) or fall back to a PID-derived marker. The fence files themselves
|
|
@@ -56,19 +79,6 @@ const ownAgentId = process.env.SO_WAVE_AGENT_ID ?? null;
|
|
|
56
79
|
*/
|
|
57
80
|
const ALL_PATHS_MARKER = '*';
|
|
58
81
|
|
|
59
|
-
/**
|
|
60
|
-
* Build a regex that finds a staged path inside a `git add` command string.
|
|
61
|
-
* Word-boundary on both sides so `src/foo.ts` does not match `src/foo.ts.bak`.
|
|
62
|
-
* The path is escaped so glob metacharacters (`*`, `?`) and shell metas
|
|
63
|
-
* cannot be reinterpreted.
|
|
64
|
-
*
|
|
65
|
-
* LEGACY PATH ONLY since #1404 — see the fallback branch in findOverlaps.
|
|
66
|
-
*/
|
|
67
|
-
function pathRegex(p) {
|
|
68
|
-
const escaped = p.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
69
|
-
return new RegExp(`(?:^|[\\s'"])${escaped}(?:$|[\\s'"])`);
|
|
70
|
-
}
|
|
71
|
-
|
|
72
82
|
/**
|
|
73
83
|
* Normalise a path token so reader and writer compare the same spelling.
|
|
74
84
|
*
|
|
@@ -107,19 +117,13 @@ function pathsOverlap(fencePath, ourPath) {
|
|
|
107
117
|
* Walk a single sibling fence file and return the staged paths a sibling
|
|
108
118
|
* agent also recorded an intent to stage.
|
|
109
119
|
*
|
|
110
|
-
*
|
|
111
|
-
*
|
|
112
|
-
*
|
|
113
|
-
*
|
|
114
|
-
*
|
|
115
|
-
*
|
|
116
|
-
*
|
|
117
|
-
* for an entry that has `command` and no `paths`.
|
|
118
|
-
* NAMED CEILING (BV-004): this fallback exists only to span the sessions
|
|
119
|
-
* running across the upgrade. REVISIT TRIGGER — remove it one minor
|
|
120
|
-
* release after 5.3.0 (i.e. in 5.4.0), by which point no process started
|
|
121
|
-
* before the upgrade can still be writing fence files. The command text it
|
|
122
|
-
* reads is never PRINTED (issue #1404) — only the fact of the match is.
|
|
120
|
+
* One entry shape is read: the #1404 `{ paths, command_hash, timestamp }`,
|
|
121
|
+
* whose path LIST is compared against our staged set. An entry without a
|
|
122
|
+
* `paths` array is skipped — that includes the pre-#1404 `{ command }` shape,
|
|
123
|
+
* whose raw-command regex fallback was removed in 5.4.x as its own revisit
|
|
124
|
+
* trigger demanded (#1487 point 16): the only fence writer,
|
|
125
|
+
* hooks/pre-bash-staging-fence.mjs, has written `paths` + `command_hash` and
|
|
126
|
+
* never `command` since 5.3.0, and no other hook or bridge writes fence files.
|
|
123
127
|
*/
|
|
124
128
|
function findOverlaps(fenceJsonPath, ourStaged) {
|
|
125
129
|
let body;
|
|
@@ -135,36 +139,159 @@ function findOverlaps(fenceJsonPath, ourStaged) {
|
|
|
135
139
|
const siblingAgent = body.agent_id ?? '<unknown>';
|
|
136
140
|
const matches = [];
|
|
137
141
|
for (const entry of body.staged_paths) {
|
|
138
|
-
if (Array.isArray(entry?.paths))
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
}
|
|
142
|
+
if (!Array.isArray(entry?.paths)) continue;
|
|
143
|
+
const hash = typeof entry.command_hash === 'string' ? entry.command_hash : '<no-hash>';
|
|
144
|
+
for (const raw of entry.paths) {
|
|
145
|
+
if (typeof raw !== 'string') continue;
|
|
146
|
+
const fencePath = normalizeStagedPath(raw);
|
|
147
|
+
for (const ours of ourStaged) {
|
|
148
|
+
if (pathsOverlap(fencePath, normalizeStagedPath(ours))) {
|
|
149
|
+
matches.push({ ourPath: ours, siblingAgent, fencePath, hash });
|
|
147
150
|
}
|
|
148
151
|
}
|
|
149
|
-
continue;
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
const cmd = entry?.command;
|
|
153
|
-
if (typeof cmd !== 'string') continue;
|
|
154
|
-
for (const ours of ourStaged) {
|
|
155
|
-
if (pathRegex(ours).test(cmd)) {
|
|
156
|
-
matches.push({
|
|
157
|
-
ourPath: ours,
|
|
158
|
-
siblingAgent,
|
|
159
|
-
fencePath: '<legacy entry, pre-#1404>',
|
|
160
|
-
hash: '<legacy entry, pre-#1404>',
|
|
161
|
-
});
|
|
162
|
-
}
|
|
163
152
|
}
|
|
164
153
|
}
|
|
165
154
|
return matches;
|
|
166
155
|
}
|
|
167
156
|
|
|
157
|
+
/** mtime of a file in ms, or `null` when it cannot be stat'ed. */
|
|
158
|
+
function mtimeMsOf(file) {
|
|
159
|
+
try {
|
|
160
|
+
return statSync(file).mtimeMs;
|
|
161
|
+
} catch {
|
|
162
|
+
return null;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Sub-mode B's decision, pure: which lines to print and whether to block.
|
|
168
|
+
*
|
|
169
|
+
* Until #1493.1 an empty `allowedPaths` skipped the check outright, the
|
|
170
|
+
* manifest's `role` and `enforcement: "off"` were never read, and a PEER
|
|
171
|
+
* session's manifest governed this session's commits. Each rung below closes
|
|
172
|
+
* one of those; the order is load-bearing — ownership first (a foreign
|
|
173
|
+
* manifest's enforcement level is not ours to honour), then `off`, then the
|
|
174
|
+
* verdict. `warn` softens ONLY the empty-union verdict; a non-empty violation
|
|
175
|
+
* blocks at every level (header § Behavior summary).
|
|
176
|
+
*
|
|
177
|
+
* @param {object} p
|
|
178
|
+
* @param {Record<string, unknown>} p.scope parsed manifest
|
|
179
|
+
* @param {string[]} p.stagedFiles repo-relative, as `git diff --cached` prints them
|
|
180
|
+
* @param {(f: string) => string|null} [p.scopePathOf] a staged path as the
|
|
181
|
+
* manifest names it — relative to the SESSION root; identity when that is the
|
|
182
|
+
* repo root. `null` = the path lies OUTSIDE the session root: a violation
|
|
183
|
+
* without pattern matching, since a `../`-prefixed path would match a glob
|
|
184
|
+
* like `**\/*.mjs` and enforce-scope denies every write outside its root
|
|
185
|
+
* (REQ-04).
|
|
186
|
+
* @param {string} p.scopeRel manifest path for messages, repo-relative
|
|
187
|
+
* @param {Set<string>} p.ownIds this process's session ids
|
|
188
|
+
* @param {number|null} p.scopeMtimeMs
|
|
189
|
+
* @param {number|null} p.sessionAge ms since the newest session clock
|
|
190
|
+
* @returns {{action: 'pass'|'report'|'block', lines: string[]}}
|
|
191
|
+
*/
|
|
192
|
+
function scopeCommitVerdict({ scope, stagedFiles, scopePathOf = (f) => f, scopeRel, ownIds, scopeMtimeMs, sessionAge }) {
|
|
193
|
+
if (stagedFiles.length === 0) return { action: 'pass', lines: [] };
|
|
194
|
+
|
|
195
|
+
// Same classifier and same disposition as enforce-scope Gate 3b: only a
|
|
196
|
+
// PROVABLY foreign manifest stands down; unknown ownership enforces.
|
|
197
|
+
if (classifyManifestSession(scope, ownIds).verdict === 'foreign') {
|
|
198
|
+
return {
|
|
199
|
+
action: 'pass',
|
|
200
|
+
lines: [`wave-scope-commit-guard: ${scopeRel} belongs to another session — its allowedPaths do not apply to this commit.`],
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const enforcement = scope.enforcement ?? 'strict';
|
|
205
|
+
if (enforcement === 'off') return { action: 'pass', lines: [] };
|
|
206
|
+
|
|
207
|
+
const allowedPaths = Array.isArray(scope.allowedPaths) ? scope.allowedPaths : [];
|
|
208
|
+
if (allowedPaths.length > 0) {
|
|
209
|
+
const violations = stagedFiles.filter(
|
|
210
|
+
(f) => {
|
|
211
|
+
const scoped = scopePathOf(f);
|
|
212
|
+
return scoped === null || !allowedPaths.some((pattern) => pathMatchesPattern(scoped, pattern));
|
|
213
|
+
},
|
|
214
|
+
);
|
|
215
|
+
if (violations.length === 0) return { action: 'pass', lines: [] };
|
|
216
|
+
// Blocks under `warn` too — see the header: in a warn-mode wave this is
|
|
217
|
+
// the only hard stop a lint-staged sweep meets before the commit exists.
|
|
218
|
+
return {
|
|
219
|
+
action: 'block',
|
|
220
|
+
lines: [
|
|
221
|
+
'✗ wave-scope-commit-guard: staged paths outside wave-scope.allowedPaths:',
|
|
222
|
+
...violations.map((v) => ` - ${v}`),
|
|
223
|
+
'',
|
|
224
|
+
'These files were likely added by lint-staged eslint --fix / prettier --write.',
|
|
225
|
+
'To proceed:',
|
|
226
|
+
' 1) git restore --staged <path> # for each foreign path',
|
|
227
|
+
' 2) git commit # retry',
|
|
228
|
+
],
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// From here on the verdict judges the coordinator's own bookkeeping (an empty
|
|
233
|
+
// union), not a staged foreign file — only this part follows `enforcement`.
|
|
234
|
+
const failAction = enforcement === 'strict' ? 'block' : 'report';
|
|
235
|
+
|
|
236
|
+
const role = typeof scope.role === 'string' ? scope.role.trim() : '';
|
|
237
|
+
const reason = classifyEmptyScope({
|
|
238
|
+
role,
|
|
239
|
+
parseOk: true,
|
|
240
|
+
scopeMtimeMs,
|
|
241
|
+
sessionStartMs: typeof sessionAge === 'number' ? Date.now() - sessionAge : null,
|
|
242
|
+
});
|
|
243
|
+
if (reason === 'read-only-role') {
|
|
244
|
+
// No wave agent commits (PSA-007), so a commit under a read-only manifest
|
|
245
|
+
// is the coordinator's own, between waves.
|
|
246
|
+
return {
|
|
247
|
+
action: 'pass',
|
|
248
|
+
lines: [`wave-scope-commit-guard: ${scopeRel} is a ${role} (read-only) manifest — treated as a coordinator commit between waves; allowedPaths not checked.`],
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
if (reason === 'stale-manifest') {
|
|
252
|
+
// Older than the newest session clock of this working copy: a leftover OR a
|
|
253
|
+
// parallel session's live manifest, which the clocks cannot tell apart
|
|
254
|
+
// (scope-gate.mjs § "…and the reader may not be the OWNER") — so it is
|
|
255
|
+
// never grounds to block this commit.
|
|
256
|
+
return {
|
|
257
|
+
action: 'report',
|
|
258
|
+
lines: [`⚠ wave-scope-commit-guard: ${scopeRel} grants no paths and predates the newest session start in this working copy — a leftover or a parallel session's live manifest; allowedPaths not checked for this commit.`],
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
return {
|
|
262
|
+
action: failAction,
|
|
263
|
+
lines: [
|
|
264
|
+
`${failAction === 'block' ? '✗' : '⚠'} wave-scope-commit-guard: ${scopeRel} grants no paths${role ? ` for role \`${role}\`` : ''} — the coordinator's \`--union\` step did not complete, so none of the ${stagedFiles.length} staged path(s) can be checked against this wave's scope.`,
|
|
265
|
+
'Re-run the union step (skills/wave-executor/references/wave-loop-scope-manifest.md § 3.3), then retry the commit; do not hand-edit allowedPaths.',
|
|
266
|
+
...(failAction === 'block' ? [] : ['(enforcement: warn — reported, the commit proceeds)']),
|
|
267
|
+
],
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* The session root a commit at `repoRoot` is judged against when it lies
|
|
273
|
+
* STRICTLY inside the repo, else `null` (judge at `repoRoot`, as before) — see
|
|
274
|
+
* the call site in `main()` (#1511 point d). Both sides realpath-resolved, so
|
|
275
|
+
* macOS `/var` vs `/private/var` cannot split one directory in two.
|
|
276
|
+
*
|
|
277
|
+
* @param {string} repoRoot `git rev-parse --show-toplevel`
|
|
278
|
+
* @returns {{sessionRoot: string, realRepoRoot: string}|null}
|
|
279
|
+
*/
|
|
280
|
+
function subdirSessionRoot(repoRoot) {
|
|
281
|
+
const canon = (p) => {
|
|
282
|
+
// `.native` canonicalises letter case on case-insensitive APFS (as
|
|
283
|
+
// enforce-scope's clamp does since #1504 pt 8); the JS realpath keeps a
|
|
284
|
+
// case-different CLAUDE_PROJECT_DIR verbatim, which relativises to `../..`
|
|
285
|
+
// against the git toplevel and switches the subdirectory mode off silently.
|
|
286
|
+
try { return realpathSync.native(p); } catch { return p; }
|
|
287
|
+
};
|
|
288
|
+
const realRepoRoot = canon(repoRoot);
|
|
289
|
+
const sessionRoot = canon(resolveSessionRoot(repoRoot, repoRoot));
|
|
290
|
+
const rel = relative(realRepoRoot, sessionRoot);
|
|
291
|
+
if (rel === '' || rel === '..' || rel.startsWith(`..${sep}`) || isAbsolute(rel)) return null;
|
|
292
|
+
return { sessionRoot, realRepoRoot };
|
|
293
|
+
}
|
|
294
|
+
|
|
168
295
|
/**
|
|
169
296
|
* Both sub-modes, in the order the pre-commit hook needs them. Every statement
|
|
170
297
|
* here used to sit at module top level, which meant a bare `import()` of this
|
|
@@ -181,7 +308,42 @@ async function main() {
|
|
|
181
308
|
// skills/wave-executor/wave-loop.md), so this guard never fired. findScopeFile
|
|
182
309
|
// returns null when no scope file exists at any precedence dir, preserving
|
|
183
310
|
// the "no active wave → exit 0" semantics below.
|
|
184
|
-
|
|
311
|
+
//
|
|
312
|
+
// #1504 point 6: the first manifest that is not provably a peer's, so a
|
|
313
|
+
// peer's higher-precedence manifest no longer hides this session's own one;
|
|
314
|
+
// when every candidate is foreign the first comes back and
|
|
315
|
+
// `scopeCommitVerdict` passes it as before. The git process inherits the
|
|
316
|
+
// committing session's env; there is no hook payload here, so the env tier is
|
|
317
|
+
// the only process-local identity.
|
|
318
|
+
const ownIds = new Set(readProcessLocalSessionIds());
|
|
319
|
+
|
|
320
|
+
// #1511 point d — ALSO read the manifest at the SESSION root the scope guards
|
|
321
|
+
// resolve (`resolveSessionRoot`, enforce-scope.mjs): a session launched in a
|
|
322
|
+
// repo SUBDIRECTORY keeps its manifest there, so the repo root held none and
|
|
323
|
+
// sub-mode B never ran for it. git runs this hook at the worktree top level
|
|
324
|
+
// and the committing session's env (the launch dir) is inherited, so the
|
|
325
|
+
// toplevel stands in for the payload `cwd` the Edit/Write guards get. Only a
|
|
326
|
+
// root STRICTLY inside the repo is adopted: the one rung that answers above it
|
|
327
|
+
// (a harness `agent-<hex>` worktree lifted to its launch checkout) would judge
|
|
328
|
+
// this repo's staged paths against another tree's manifest.
|
|
329
|
+
//
|
|
330
|
+
// The repo-root manifest is ALWAYS judged exactly as before #1511 d, and the
|
|
331
|
+
// subdirectory manifest is judged IN ADDITION, each against its own path
|
|
332
|
+
// mapping; either one blocking blocks. So the verdict is never laxer than the
|
|
333
|
+
// repo-root-only guard it extends — choosing ONE of the two manifests let an
|
|
334
|
+
// unbound or `enforcement: off` subdirectory manifest silence a repo-root wave
|
|
335
|
+
// that blocked the same commit (second review, MED-1).
|
|
336
|
+
const subdir = subdirSessionRoot(repoRoot);
|
|
337
|
+
const judgements = [{ root: repoRoot, scopePathOf: (f) => f }];
|
|
338
|
+
if (subdir !== null) {
|
|
339
|
+
judgements.push({
|
|
340
|
+
root: subdir.sessionRoot,
|
|
341
|
+
scopePathOf: (f) => {
|
|
342
|
+
const rel = relative(subdir.sessionRoot, join(subdir.realRepoRoot, f));
|
|
343
|
+
return rel === '..' || rel.startsWith(`..${sep}`) || isAbsolute(rel) ? null : rel;
|
|
344
|
+
},
|
|
345
|
+
});
|
|
346
|
+
}
|
|
185
347
|
const fenceDir = join(repoRoot, '.orchestrator', 'staging-fence');
|
|
186
348
|
|
|
187
349
|
// -------------------------------------------------------------------------
|
|
@@ -191,7 +353,11 @@ async function main() {
|
|
|
191
353
|
const stagedOutput = execSync('git diff --cached --name-only', { encoding: 'utf8' });
|
|
192
354
|
const stagedFiles = stagedOutput.split('\n').filter(Boolean);
|
|
193
355
|
|
|
194
|
-
|
|
356
|
+
let blocked = false;
|
|
357
|
+
for (const { root, scopePathOf } of judgements) {
|
|
358
|
+
const located = findOwnScopeFile(root, ownIds, classifyManifestSession);
|
|
359
|
+
const scopePath = located.path ?? located.foreignPath;
|
|
360
|
+
if (!scopePath) continue;
|
|
195
361
|
let scope;
|
|
196
362
|
try {
|
|
197
363
|
scope = JSON.parse(readFileSync(scopePath, 'utf8'));
|
|
@@ -200,23 +366,19 @@ async function main() {
|
|
|
200
366
|
process.exit(1);
|
|
201
367
|
}
|
|
202
368
|
|
|
203
|
-
const
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
)
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
process.stderr.write(' 1) git restore --staged <path> # for each foreign path\n');
|
|
215
|
-
process.stderr.write(' 2) git commit # retry\n');
|
|
216
|
-
process.exit(1);
|
|
217
|
-
}
|
|
218
|
-
}
|
|
369
|
+
const verdict = scopeCommitVerdict({
|
|
370
|
+
scope: scope !== null && typeof scope === 'object' ? scope : {},
|
|
371
|
+
stagedFiles,
|
|
372
|
+
scopePathOf,
|
|
373
|
+
scopeRel: relative(repoRoot, scopePath) || scopePath,
|
|
374
|
+
ownIds,
|
|
375
|
+
scopeMtimeMs: mtimeMsOf(scopePath),
|
|
376
|
+
sessionAge: sessionAgeMs(root),
|
|
377
|
+
});
|
|
378
|
+
for (const line of verdict.lines) process.stderr.write(`${line}\n`);
|
|
379
|
+
if (verdict.action === 'block') blocked = true;
|
|
219
380
|
}
|
|
381
|
+
if (blocked) process.exit(1);
|
|
220
382
|
|
|
221
383
|
// -------------------------------------------------------------------------
|
|
222
384
|
// Sub-mode C — cross-agent staging-fence reconciliation (issue #552)
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "session-orchestrator",
|
|
3
|
-
"version": "5.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "5.6.0",
|
|
4
|
+
"description": "Your coding agents get an agreed plan, a check after each round of changes and a handover to the next session. Plan, Go, Close: /session reads your repo and agrees the scope, /go runs the work in waves with your tests and typecheck after each implementation wave, /close runs the full gate, verifies, commits and files the rest as issues. Scope and destructive-command guards, crash-safe resume, learnings you approve. For Claude Code, Codex CLI, Cursor and Pi.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"homepage": "https://session-orchestrator.com",
|
|
7
7
|
"keywords": [
|
|
@@ -16,7 +16,12 @@
|
|
|
16
16
|
"orchestration",
|
|
17
17
|
"developer-tools",
|
|
18
18
|
"mcp",
|
|
19
|
-
"llm"
|
|
19
|
+
"llm",
|
|
20
|
+
"workflow",
|
|
21
|
+
"quality-gates",
|
|
22
|
+
"ai-coding",
|
|
23
|
+
"gitlab",
|
|
24
|
+
"github"
|
|
20
25
|
],
|
|
21
26
|
"engines": {
|
|
22
27
|
"node": ">=24.0.0"
|
|
@@ -54,6 +59,7 @@
|
|
|
54
59
|
"!**/.DS_Store"
|
|
55
60
|
],
|
|
56
61
|
"scripts": {
|
|
62
|
+
"update:local": "node scripts/self-update.mjs",
|
|
57
63
|
"test": "node scripts/check-package-manager.mjs && vitest --run",
|
|
58
64
|
"test:coverage": "node scripts/check-package-manager.mjs && vitest --run --coverage",
|
|
59
65
|
"test:watch": "node scripts/check-package-manager.mjs && vitest",
|
|
@@ -92,11 +98,6 @@
|
|
|
92
98
|
"prettier": "^3.0.0",
|
|
93
99
|
"vitest": "^4.1.11"
|
|
94
100
|
},
|
|
95
|
-
"overrides": {
|
|
96
|
-
"vite": "^8.0.16",
|
|
97
|
-
"fast-uri": "^3.1.8",
|
|
98
|
-
"brace-expansion": "^5.0.12"
|
|
99
|
-
},
|
|
100
101
|
"license": "MIT",
|
|
101
102
|
"repository": {
|
|
102
103
|
"type": "git",
|
|
@@ -62,18 +62,22 @@
|
|
|
62
62
|
* Telemetry: `--apply` emits `orchestrator.learnings.session_write_applied`
|
|
63
63
|
* after the write — its presence is the proof the write ran. Best-effort: a
|
|
64
64
|
* telemetry failure never changes the exit code. Skipped (stderr WARN) when
|
|
65
|
-
* `--file` lies outside `--repo-root
|
|
65
|
+
* `--file` lies outside `--repo-root` after resolving symlinks on both sides,
|
|
66
|
+
* so the record is never pinned to a repo
|
|
66
67
|
* whose store was not written.
|
|
67
68
|
*
|
|
68
69
|
* Exit codes:
|
|
69
70
|
* 0 Success (including a no-op)
|
|
70
71
|
* 1 Usage/input/validation error — nothing written
|
|
71
|
-
* 2 Library/IO error (unreadable store,
|
|
72
|
-
*
|
|
72
|
+
* 2 Library/IO error (unreadable store, a failure inside the
|
|
73
|
+
* prune/rewrite, store lock not acquired). Unparseable store lines are
|
|
74
|
+
* NOT an error: they are kept verbatim, counted as `malformed` in the
|
|
75
|
+
* summary, and WARNed on stderr with their line numbers.
|
|
73
76
|
*/
|
|
74
77
|
|
|
75
|
-
import { readFileSync } from 'node:fs';
|
|
78
|
+
import { readFileSync, realpathSync } from 'node:fs';
|
|
76
79
|
import path from 'node:path';
|
|
80
|
+
import { validatePathInsideProject } from './lib/path-utils.mjs';
|
|
77
81
|
import { LearningsLockError, readLearnings, withLearningsLock } from './lib/learnings/io.mjs';
|
|
78
82
|
import { pruneLearnings } from './lib/learnings/expiry-sweep.mjs';
|
|
79
83
|
import {
|
|
@@ -310,9 +314,29 @@ export function buildNextGeneration({ current, updates, newLearnings, decayRate,
|
|
|
310
314
|
return { next, confirmed, contradicted, appended: newIds.size, decayed, undecayable };
|
|
311
315
|
}
|
|
312
316
|
|
|
317
|
+
/**
|
|
318
|
+
* A path resolved through symlinks when it exists, else absolute as given.
|
|
319
|
+
*
|
|
320
|
+
* @param {string} p
|
|
321
|
+
* @returns {string}
|
|
322
|
+
*/
|
|
323
|
+
function canonicalPath(p) {
|
|
324
|
+
try {
|
|
325
|
+
return realpathSync(p);
|
|
326
|
+
} catch {
|
|
327
|
+
return path.resolve(p);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
313
331
|
async function emitSessionWriteApplied({ repoRoot, filePath, payload }) {
|
|
314
|
-
|
|
315
|
-
|
|
332
|
+
// Canonical, not lexical (#1506.4b; same check as sweep-expired-learnings
|
|
333
|
+
// --drop-malformed, #1500 F4): a store reached through a symlinked directory
|
|
334
|
+
// inside the repo but living outside it is refused, and a root spelled
|
|
335
|
+
// through a symlink (macOS /var → /private/var) still accepts the store
|
|
336
|
+
// inside it. Runs after the write, so the store exists and resolves.
|
|
337
|
+
const file = canonicalPath(filePath);
|
|
338
|
+
const rel = path.relative(canonicalPath(repoRoot), file);
|
|
339
|
+
if (!validatePathInsideProject(file, repoRoot, { canonicalizeRoot: true }).ok) {
|
|
316
340
|
process.stderr.write(
|
|
317
341
|
`apply-session-learnings: skipped ${SESSION_WRITE_EVENT} — ${filePath} lies outside ` +
|
|
318
342
|
`--repo-root ${repoRoot}\n`,
|
|
@@ -377,14 +401,11 @@ async function main(argv) {
|
|
|
377
401
|
process.stderr.write(`apply-session-learnings: cannot read ${filePath}: ${err.message}\n`);
|
|
378
402
|
return { code: 2 };
|
|
379
403
|
}
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
);
|
|
386
|
-
return { code: 2 };
|
|
387
|
-
}
|
|
404
|
+
// A malformed store line is no reason to refuse (#1489): pruneLearnings()
|
|
405
|
+
// keeps it verbatim at the end of the rewritten store, never archives it,
|
|
406
|
+
// and prints the WARN; the count rides on the summary as `malformed`.
|
|
407
|
+
// Refusing here blocked the session-end learnings write on every close,
|
|
408
|
+
// because nothing on the sanctioned write path ever removes such a line.
|
|
388
409
|
|
|
389
410
|
let built;
|
|
390
411
|
try {
|
|
@@ -44,7 +44,7 @@ import { readFileSync } from 'node:fs';
|
|
|
44
44
|
import { spawnSync } from 'node:child_process';
|
|
45
45
|
import { join } from 'node:path';
|
|
46
46
|
|
|
47
|
-
import { findProjectRoot, resolveInstructionFile, warn } from './lib/common.mjs';
|
|
47
|
+
import { findProjectRoot, redactHomeDir, resolveInstructionFile, warn } from './lib/common.mjs';
|
|
48
48
|
import { parseSessionConfig } from './lib/config.mjs';
|
|
49
49
|
import { glabRun as defaultGlabRun } from './lib/vault-backfill/glab.mjs';
|
|
50
50
|
import { archiveFileToVault, titleFromMarkdown } from './lib/vault-archive.mjs';
|
|
@@ -381,8 +381,13 @@ EXIT CODES
|
|
|
381
381
|
}
|
|
382
382
|
|
|
383
383
|
function printHuman(archived, skipped, isDryRun, vaultDir, vaultSubdir) {
|
|
384
|
+
// Runs as a custom phase at every session start and end, so this line lands
|
|
385
|
+
// in every transcript — home dir as `~` (#1490). `--json` keeps the real
|
|
386
|
+
// `vaultDir`: that field is a machine contract, not a log line. A
|
|
387
|
+
// `skipped[].reason` is redacted in BOTH outputs — it carries fs error text,
|
|
388
|
+
// and main() redacts it before it enters the array (8273d97a).
|
|
384
389
|
process.stdout.write(
|
|
385
|
-
`Doc archive ${isDryRun ? '(dry-run)' : '(apply)'} → ${vaultDir}/${vaultSubdir}\n`,
|
|
390
|
+
`Doc archive ${isDryRun ? '(dry-run)' : '(apply)'} → ${redactHomeDir(`${vaultDir}/${vaultSubdir}`)}\n`,
|
|
386
391
|
);
|
|
387
392
|
if (archived.length > 0) {
|
|
388
393
|
process.stdout.write(` ${isDryRun ? 'WOULD ARCHIVE' : 'ARCHIVED'} (${archived.length}):\n`);
|
|
@@ -488,19 +493,38 @@ export function main({
|
|
|
488
493
|
const root = repoRoot ?? findProjectRoot();
|
|
489
494
|
const instr = resolveInstructionFile(root);
|
|
490
495
|
if (!instr) {
|
|
491
|
-
process.stderr.write(`archive-closed-prds: no CLAUDE.md/AGENTS.md at ${root}.\n`);
|
|
496
|
+
process.stderr.write(`archive-closed-prds: no CLAUDE.md/AGENTS.md at ${redactHomeDir(root)}.\n`);
|
|
492
497
|
return { code: 1, archived: [], skipped: [] };
|
|
493
498
|
}
|
|
494
499
|
|
|
495
500
|
let vaultDir;
|
|
501
|
+
let vaultIntegration;
|
|
496
502
|
try {
|
|
497
503
|
const content = readFileSync(instr.path, 'utf8');
|
|
498
504
|
const config = parseSessionConfig(content, hostPaths ? { hostPaths } : undefined);
|
|
499
|
-
|
|
505
|
+
vaultIntegration = config?.['vault-integration'];
|
|
506
|
+
vaultDir = vaultIntegration?.['vault-dir'];
|
|
500
507
|
} catch (err) {
|
|
501
|
-
process.stderr.write(`archive-closed-prds: failed to parse Session Config: ${err.message}\n`);
|
|
508
|
+
process.stderr.write(`archive-closed-prds: failed to parse Session Config: ${redactHomeDir(err.message)}\n`);
|
|
502
509
|
return { code: 2, archived: [], skipped: [] };
|
|
503
510
|
}
|
|
511
|
+
// The switch every vault writer obeys (board-writer, narrative-mirror): off
|
|
512
|
+
// → nothing archived, exit 0. parseSessionConfig has already applied the
|
|
513
|
+
// host-local lowering (resolveVaultIntegrationHost: env SO_VAULT_INTEGRATION
|
|
514
|
+
// > owner.yaml > committed). This runs with --apply at every session start
|
|
515
|
+
// and end, so ignoring it wrote PRDs into the vault and git-rm'd them (#1496).
|
|
516
|
+
if (vaultIntegration?.enabled !== true) {
|
|
517
|
+
const override = vaultIntegration?.['host-override'];
|
|
518
|
+
const disabled = `vault-integration is ${override ? `switched off by ${override}` : 'not enabled'} — nothing archived`;
|
|
519
|
+
if (json) {
|
|
520
|
+
process.stdout.write(
|
|
521
|
+
JSON.stringify({ dryRun: isDryRun, vaultDir: null, vaultSubdir, archived: [], skipped: [], disabled }, null, 2) + '\n',
|
|
522
|
+
);
|
|
523
|
+
} else {
|
|
524
|
+
process.stdout.write(`Doc archive: ${disabled}\n`);
|
|
525
|
+
}
|
|
526
|
+
return { code: 0, archived: [], skipped: [] };
|
|
527
|
+
}
|
|
504
528
|
if (!vaultDir || typeof vaultDir !== 'string' || vaultDir.trim() === '') {
|
|
505
529
|
process.stderr.write(
|
|
506
530
|
'archive-closed-prds: vault-integration.vault-dir is not configured — cannot archive.\n',
|
|
@@ -534,8 +558,10 @@ export function main({
|
|
|
534
558
|
try {
|
|
535
559
|
header = readHeaderRegion(abs);
|
|
536
560
|
} catch (err) {
|
|
537
|
-
|
|
538
|
-
|
|
561
|
+
// fs errors name the ABSOLUTE path they failed on (#1490).
|
|
562
|
+
const msg = redactHomeDir(err.message);
|
|
563
|
+
skipped.push({ source: rel, reason: `unreadable: ${msg}` });
|
|
564
|
+
warn(`archive-closed-prds: cannot read ${rel}: ${msg}`);
|
|
539
565
|
continue;
|
|
540
566
|
}
|
|
541
567
|
|
|
@@ -603,8 +629,9 @@ export function main({
|
|
|
603
629
|
title: titleFromMarkdown(header),
|
|
604
630
|
});
|
|
605
631
|
} catch (err) {
|
|
606
|
-
|
|
607
|
-
|
|
632
|
+
const msg = redactHomeDir(err.message);
|
|
633
|
+
skipped.push({ source: rel, reason: `archive-failed: ${msg}`, iid, via, ownership });
|
|
634
|
+
warn(`archive-closed-prds: failed to archive ${rel}: ${msg}`);
|
|
608
635
|
continue;
|
|
609
636
|
}
|
|
610
637
|
entry.iid = iid;
|
|
@@ -614,7 +641,7 @@ export function main({
|
|
|
614
641
|
if (!isDryRun) {
|
|
615
642
|
const rm = gitRunFn(['-C', root, 'rm', '--', rel]);
|
|
616
643
|
entry.removed = rm.ok;
|
|
617
|
-
if (!rm.ok) warn(`archive-closed-prds: 'git rm ${rel}' failed: ${rm.stderr.trim()}`);
|
|
644
|
+
if (!rm.ok) warn(`archive-closed-prds: 'git rm ${rel}' failed: ${redactHomeDir(rm.stderr.trim())}`);
|
|
618
645
|
}
|
|
619
646
|
|
|
620
647
|
archived.push(entry);
|
|
@@ -641,7 +668,7 @@ if (isMainModule(import.meta.url)) {
|
|
|
641
668
|
const { code } = main();
|
|
642
669
|
process.exit(code ?? 0);
|
|
643
670
|
} catch (err) {
|
|
644
|
-
process.stderr.write(`archive-closed-prds: unexpected error: ${err?.stack ?? err}\n`);
|
|
671
|
+
process.stderr.write(`archive-closed-prds: unexpected error: ${redactHomeDir(String(err?.stack ?? err))}\n`);
|
|
645
672
|
process.exit(2);
|
|
646
673
|
}
|
|
647
674
|
}
|
package/scripts/autopilot.mjs
CHANGED
|
@@ -400,12 +400,19 @@ async function main() {
|
|
|
400
400
|
});
|
|
401
401
|
|
|
402
402
|
const sessionIds = state.sessions.join(', ') || '(none)';
|
|
403
|
+
// #1498: `tokens_unknown_sessions` (#1487 Pkt 4) reached state, return value
|
|
404
|
+
// and autopilot.jsonl, but no report showed it — so the token total read as
|
|
405
|
+
// the whole sum. Printed only when non-zero: then the total is a lower bound.
|
|
406
|
+
const unknownTokens = state.tokens_unknown_sessions > 0
|
|
407
|
+
? ` tokens_unknown_sessions=${state.tokens_unknown_sessions} (the token total is a lower bound)`
|
|
408
|
+
: '';
|
|
403
409
|
const summary =
|
|
404
410
|
`${state.autopilot_run_id} ` +
|
|
405
411
|
`kill_switch=${state.kill_switch ?? 'none'} ` +
|
|
406
412
|
`iterations=${state.iterations_completed} ` +
|
|
407
413
|
`sessions=[${sessionIds}] ` +
|
|
408
|
-
`duration=${state.duration_seconds}s
|
|
414
|
+
`duration=${state.duration_seconds}s ` +
|
|
415
|
+
`tokens=${state.total_tokens_used}${unknownTokens}`;
|
|
409
416
|
process.stdout.write(summary + '\n');
|
|
410
417
|
|
|
411
418
|
if (state.kill_switch === KILL_SWITCHES.FAILED_WAVE) {
|