session-orchestrator 4.1.0 → 5.0.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/.agents/skills/session-plan/SKILL.md +1 -1
- package/.agents/skills/session-start/SKILL.md +1 -1
- package/.agents/skills/ux-grill/SKILL.md +22 -0
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +3 -2
- package/.codex-plugin/plugin.json +1 -1
- package/.codex-plugin/skills/session-plan/SKILL.md +1 -1
- package/.codex-plugin/skills/session-start/SKILL.md +1 -1
- package/.codex-plugin/skills/ux-grill/SKILL.md +21 -0
- package/.codex-plugin/skills/ux-grill/agents/openai.yaml +5 -0
- package/.cursor/commands/ux-grill.md +14 -0
- package/.cursor/skills/session-plan/SKILL.md +1 -1
- package/.cursor/skills/session-start/SKILL.md +1 -1
- package/.cursor/skills/ux-grill/SKILL.md +13 -0
- package/.cursor-plugin/plugin.json +1 -1
- package/AGENTS.md +2 -1
- package/CHANGELOG.md +128 -1
- package/README.md +98 -86
- package/agents/dialectic-deriver.md +11 -0
- package/agents/ux-evaluator.md +1 -1
- package/commands/close.md +3 -3
- package/commands/go.md +2 -0
- package/commands/memory-cleanup.md +4 -3
- package/commands/persona-panel.md +1 -1
- package/commands/session.md +3 -2
- package/commands/ux-grill.md +51 -0
- package/docs/README.md +4 -4
- package/docs/USER-GUIDE.md +117 -50
- package/docs/agent-authoring.md +2 -2
- package/docs/baseline.md +55 -1
- package/docs/ci-setup.md +1 -1
- package/docs/codex-setup.md +9 -0
- package/docs/components.md +9 -9
- package/docs/cursor-setup.md +1 -0
- package/docs/events-schema.md +13 -6
- package/docs/github-mirror-protection.md +61 -20
- package/docs/instruction-delivery.md +1 -1
- package/docs/memory-proposal-flow.md +3 -3
- package/docs/migration-v4.md +2 -2
- package/docs/migration-v5.md +62 -0
- package/docs/owner-config-schema.md +74 -90
- package/docs/persona-panel.md +4 -4
- package/docs/pi-setup.md +1 -0
- package/docs/rule-authoring.md +13 -6
- package/docs/scope-collision-guard.md +16 -0
- package/docs/session-config-reference.md +55 -22
- package/docs/session-config-template.md +9 -5
- package/docs/vault-docs-architecture.md +4 -2
- package/hooks/_lib/hook-import-set.json +70 -3
- package/hooks/_lib/lock-bootstrap.mjs +84 -1
- package/hooks/_lib/vcs-create-matcher.mjs +401 -16
- package/hooks/enforce-scope.mjs +201 -0
- package/hooks/hooks-codex.json +1 -1
- package/hooks/hooks-cursor.json +5 -0
- package/hooks/hooks.json +7 -2
- package/hooks/on-session-start.mjs +171 -49
- package/hooks/post-bash-issue-budget-refund.mjs +375 -0
- package/hooks/pre-auq-clarity.mjs +70 -18
- package/hooks/pre-bash-issue-budget.mjs +170 -26
- package/hooks/subagent-telemetry.mjs +106 -20
- package/package.json +5 -4
- package/pi/prompts/ux-grill.md +12 -0
- package/scripts/baseline-archetypes.mjs +28 -0
- package/scripts/ci/assert-vitest-green.mjs +4 -2
- package/scripts/dialectic-deriver.mjs +32 -8
- package/scripts/emit-session.mjs +72 -1
- package/scripts/lib/agent-status.mjs +441 -9
- package/scripts/lib/auq/schema.mjs +10 -3
- package/scripts/lib/auto-dialectic.mjs +0 -68
- package/scripts/lib/baseline-archetypes.mjs +439 -0
- package/scripts/lib/build-live-signals.mjs +5 -6
- package/scripts/lib/ci-status-banner.mjs +29 -6
- package/scripts/lib/claude-md-budget-lint.mjs +52 -2
- package/scripts/lib/config/issue-budget.mjs +68 -8
- package/scripts/lib/config/private-config-dir.mjs +3 -2
- package/scripts/lib/config/remote-hosts.mjs +2 -2
- package/scripts/lib/config-schema.mjs +79 -0
- package/scripts/lib/config.mjs +12 -1
- package/scripts/lib/eval/engine.mjs +7 -1
- package/scripts/lib/file-lock.mjs +151 -8
- package/scripts/lib/git-porcelain.mjs +113 -0
- package/scripts/lib/instruction-budget-guard.mjs +415 -47
- package/scripts/lib/io.mjs +29 -4
- package/scripts/lib/issue-budget-reconcile.mjs +392 -0
- package/scripts/lib/issue-budget.mjs +412 -9
- package/scripts/lib/learnings/evolve-telemetry.mjs +1 -2
- package/scripts/lib/learnings/sizing-subject.mjs +44 -0
- package/scripts/lib/locks/staging-fence-lock.mjs +19 -38
- package/scripts/lib/locks/state-md-lock.mjs +19 -41
- package/scripts/lib/maintenance-due-banner.mjs +450 -0
- package/scripts/lib/owner-config.example.yaml +29 -46
- package/scripts/lib/owner-yaml.mjs +14 -13
- package/scripts/lib/peer-cards/merger.mjs +143 -0
- package/scripts/lib/pre-dispatch-check.mjs +20 -14
- package/scripts/lib/project-hygiene.mjs +81 -30
- package/scripts/lib/quality-gate.mjs +27 -71
- package/scripts/lib/reconcile/engine.mjs +19 -1
- package/scripts/lib/reconcile/writer.mjs +278 -11
- package/scripts/lib/resource-probe/evaluate.mjs +19 -21
- package/scripts/lib/rules-sync.mjs +34 -4
- package/scripts/lib/scope-echo.mjs +346 -0
- package/scripts/lib/session-close-backfill.mjs +182 -40
- package/scripts/lib/session-end/phase-skip.mjs +85 -86
- package/scripts/lib/session-end/tail-runner.mjs +178 -0
- package/scripts/lib/session-lock.mjs +62 -2
- package/scripts/lib/session-record-repair.mjs +91 -0
- package/scripts/lib/session-schema/constants.mjs +6 -0
- package/scripts/lib/session-schema/filters.mjs +26 -1
- package/scripts/lib/session-schema/validator.mjs +20 -0
- package/scripts/lib/session-shape.mjs +558 -0
- package/scripts/lib/session-start-probes.mjs +429 -56
- package/scripts/lib/session-token-rollup.mjs +95 -10
- package/scripts/lib/state-md/frontmatter-mutators.mjs +22 -34
- package/scripts/lib/state-md.mjs +1 -0
- package/scripts/lib/subagents-schema.mjs +77 -9
- package/scripts/lib/telemetry/pricing.mjs +197 -0
- package/scripts/lib/telemetry/sync.mjs +50 -1
- package/scripts/lib/test-runner/artifact-paths.mjs +30 -5
- package/scripts/lib/test-runner/issue-reconcile.mjs +45 -8
- package/scripts/lib/tmux-layout/layouts.mjs +62 -4
- package/scripts/lib/ux-grill/collect.mjs +1163 -0
- package/scripts/lib/ux-grill/compare.mjs +285 -0
- package/scripts/lib/ux-grill/manifest.mjs +618 -0
- package/scripts/lib/ux-grill/measures.mjs +431 -0
- package/scripts/lib/ux-grill/paths.mjs +224 -0
- package/scripts/lib/ux-grill/pencil-coverage.mjs +284 -0
- package/scripts/lib/ux-grill/reconcile.mjs +344 -0
- package/scripts/lib/ux-grill/run-record.mjs +316 -0
- package/scripts/lib/ux-grill/schema.mjs +321 -0
- package/scripts/lib/validate/check-skill-script-paths.mjs +33 -10
- package/scripts/lib/validate/check-untracked-test-deps.mjs +33 -19
- package/scripts/lib/validate/check-unwired-features.mjs +56 -27
- package/scripts/lib/vault-mirror/process.mjs +2 -1
- package/scripts/lib/vault-status/board-lock.mjs +18 -0
- package/scripts/lib/vault-status/board-writer.mjs +8 -0
- package/scripts/lib/vault-status/narrative-mirror.mjs +4 -4
- package/scripts/lib/wave-resource-gate.mjs +23 -27
- package/scripts/lib/wave-sizing.mjs +10 -3
- package/scripts/materialize-wave-scope.mjs +68 -14
- package/scripts/mcp-server.sh +16 -1
- package/scripts/print-applicable-rules.mjs +7 -6
- package/scripts/print-learnings-index.mjs +3 -2
- package/scripts/release.mjs +7 -2
- package/scripts/session-shape.mjs +266 -0
- package/skills/_shared/config-reading.md +15 -9
- package/skills/_shared/private-capability-context.md +89 -0
- package/skills/bootstrap/SKILL.md +60 -209
- package/skills/bootstrap/_shared-template.md +99 -14
- package/skills/bootstrap/deep-template.md +36 -26
- package/skills/bootstrap/fast-template.md +44 -8
- package/skills/bootstrap/intensity-heuristic.md +10 -4
- package/skills/bootstrap/private-contract.md +119 -0
- package/skills/bootstrap/public-fallback.md +30 -18
- package/skills/bootstrap/references/bootstrap-ecosystem-health-flow.md +48 -0
- package/skills/bootstrap/references/bootstrap-refresh-lock-flow.md +37 -0
- package/skills/bootstrap/references/bootstrap-retroactive-flow.md +108 -0
- package/skills/bootstrap/references/bootstrap-rules-fetch-bridge.md +64 -0
- package/skills/bootstrap/standard-template.md +39 -24
- package/skills/claude-md-drift-check/SKILL.md +9 -2
- package/skills/claude-md-drift-check/checker.mjs +213 -21
- package/skills/discovery/SKILL.md +6 -173
- package/skills/discovery/probes/vault-staleness.mjs +35 -5
- package/skills/discovery/probes-docs.md +8 -4
- package/skills/discovery/probes-supply-chain.md +4 -2
- package/skills/discovery/probes-ui.md +8 -4
- package/skills/discovery/probes-vault.md +12 -4
- package/skills/discovery/references/discovery-interactive-triage.md +139 -0
- package/skills/discovery/references/discovery-triage-state.md +54 -0
- package/skills/docs-orchestrator/audience-mapping.md +1 -1
- package/skills/eval/rubric-v1.md +13 -0
- package/skills/evolve/SKILL.md +2 -458
- package/skills/evolve/references/evolve-analyze-mode.md +360 -0
- package/skills/evolve/references/evolve-dialectic-mode.md +139 -0
- package/skills/gitlab-ops/SKILL.md +3 -3
- package/skills/grill/SKILL.md +1 -1
- package/skills/memory-cleanup/SKILL.md +2 -2
- package/skills/plan/mode-new.md +9 -0
- package/skills/plan/mode-retro.md +4 -3
- package/skills/reconcile/SKILL.md +11 -1
- package/skills/session-end/SKILL.md +3 -2
- package/skills/session-end/drift-operations.md +20 -5
- package/skills/session-end/metrics-collection.md +1 -0
- package/skills/session-end/phase-3-2-docs-verification.md +1 -1
- package/skills/session-end/phase-3-6-tail.md +27 -67
- package/skills/session-end/phase-3-7a-recommendations.md +2 -2
- package/skills/session-end/references/phase-2-quality-gate.md +3 -3
- package/skills/session-end/references/phase-3-documentation-updates.md +8 -6
- package/skills/session-end/references/phase-5-issue-cleanup.md +32 -1
- package/skills/session-end/session-metrics-write.md +33 -12
- package/skills/session-plan/SKILL.md +46 -180
- package/skills/session-plan/references/session-plan-task-classification.md +152 -0
- package/skills/session-plan/wave-template.md +8 -15
- package/skills/session-start/SKILL.md +41 -7
- package/skills/session-start/phase-2-5-docs-planning.md +1 -1
- package/skills/session-start/phase-8-5-express-path.md +12 -9
- package/skills/session-start/references/operations-contract.md +114 -0
- package/skills/session-start/references/phase-1-5-session-continuity.md +2 -0
- package/skills/session-start/references/phase-4-ssot-environment-check.md +42 -24
- package/skills/session-start/references/phase-6-7-memory-banner-telemetry-consent.md +3 -1
- package/skills/session-start/soul.md +2 -2
- package/skills/test-runner/SKILL.md +1 -1
- package/skills/test-runner/rubric-v1.md +2 -2
- package/skills/tmux-layout/SKILL.md +3 -1
- package/skills/ux-grill/SKILL.md +211 -0
- package/skills/ux-grill/rubric-v2.md +201 -0
- package/skills/ux-grill/soul.md +76 -0
- package/skills/wave-executor/SKILL.md +32 -127
- package/skills/wave-executor/circuit-breaker.md +3 -1
- package/skills/wave-executor/references/wave-executor-quality-gate.md +61 -0
- package/skills/wave-executor/references/wave-executor-state-init.md +86 -0
- package/skills/wave-executor/references/wave-loop-dispatch.md +12 -2
- package/skills/wave-executor/references/wave-loop-review.md +19 -6
- package/skills/wave-executor/references/wave-loop-scope-manifest.md +6 -2
- package/templates/_shared/ux-manifest.template.md +149 -0
- package/templates/nextjs-minimal/package.json +1 -1
- package/templates/node-minimal/package.json +1 -1
- package/scripts/lib/multi-provider-build/providers.mjs +0 -64
- package/scripts/lib/multi-provider-build/templating.mjs +0 -130
- package/scripts/lib/owner-config/coerce.mjs +0 -29
- package/scripts/lib/owner-config/constants.mjs +0 -21
- package/scripts/lib/owner-config/defaults.mjs +0 -50
- package/scripts/lib/owner-config/error.mjs +0 -19
- package/scripts/lib/owner-config/index.mjs +0 -13
- package/scripts/lib/owner-config/merge.mjs +0 -52
- package/scripts/lib/owner-config/validate.mjs +0 -259
- package/scripts/lib/owner-config-loader.mjs +0 -170
- package/scripts/lib/owner-config.mjs +0 -28
- package/scripts/lib/soul-resolve.mjs +0 -130
- package/scripts/lib/vault-mirror/render.mjs +0 -8
- package/templates/_shared/journey-manifest.md +0 -114
|
@@ -17,10 +17,12 @@
|
|
|
17
17
|
* Decision flow:
|
|
18
18
|
* G1 tool filter — only Bash is gated.
|
|
19
19
|
* G2 command is a non-empty string.
|
|
20
|
-
* G3 matcher — `gh|glab … issue create|new`
|
|
20
|
+
* G3 matcher — `gh|glab … issue create|new` plus the REST route
|
|
21
|
+
* (`gh|glab api … /issues`, #1163) only. PR/MR creation passes.
|
|
21
22
|
* Verb-resolved since #1145, so a wrapped (`nohup`), absolute-path or
|
|
22
23
|
* env-prefixed create is seen; a `--help` invocation is not (it creates
|
|
23
|
-
* nothing).
|
|
24
|
+
* nothing). Since #1163 the matcher returns EVERY create statement of the
|
|
25
|
+
* chain, and the cap charges ONE unit per statement.
|
|
24
26
|
* G4 config — `issue-budget` from CLAUDE.md/AGENTS.md. `mode: off` → allow.
|
|
25
27
|
* G3b bulk — a create inside a shell LOOP body creates an unknowable number
|
|
26
28
|
* of issues (#1145). `strict` → deny; `warn` → allow with an explicit
|
|
@@ -29,7 +31,8 @@
|
|
|
29
31
|
* G5 exemption — priority::critical / carryover class / broken-window /
|
|
30
32
|
* the overflow collector itself bypass the cap unconditionally, keeping
|
|
31
33
|
* the session-end promises at SKILL.md:319 and :1113 intact.
|
|
32
|
-
* G6 charge the counter
|
|
34
|
+
* G6 charge the counter ONCE PER ISSUE-CREATE STATEMENT in
|
|
35
|
+
* .orchestrator/runtime/issue-budget/<hash>.json
|
|
33
36
|
* (one file per session since #1141 — see scripts/lib/issue-budget.mjs
|
|
34
37
|
* `budgetStateRel`).
|
|
35
38
|
* under cap → allow; over cap + `warn` → allow with stderr notice;
|
|
@@ -50,13 +53,17 @@
|
|
|
50
53
|
import { readStdin, emitAllow, emitDeny, emitWarn } from '../scripts/lib/io.mjs';
|
|
51
54
|
import { resolveProjectDir } from '../scripts/lib/platform.mjs';
|
|
52
55
|
import { readJson } from '../scripts/lib/common.mjs';
|
|
53
|
-
import {
|
|
56
|
+
import { findIssueCreateStatements, isLoopedIssueCreate } from './_lib/vcs-create-matcher.mjs';
|
|
54
57
|
import {
|
|
55
58
|
loadIssueBudgetConfig,
|
|
56
59
|
resolveIssueBudgetSessionId,
|
|
57
60
|
chargeIssueBudget,
|
|
58
61
|
classifyExemption,
|
|
59
62
|
formatBlockReason,
|
|
63
|
+
readBudgetState,
|
|
64
|
+
writeBudgetState,
|
|
65
|
+
budgetStatePath,
|
|
66
|
+
buildOverflowRecord,
|
|
60
67
|
} from '../scripts/lib/issue-budget.mjs';
|
|
61
68
|
|
|
62
69
|
import { shouldRunHook } from './_lib/profile-gate.mjs';
|
|
@@ -114,6 +121,25 @@ async function resolveSessionId(input, projectDir) {
|
|
|
114
121
|
return resolveIssueBudgetSessionId(nativeRawId, currentSession);
|
|
115
122
|
}
|
|
116
123
|
|
|
124
|
+
/**
|
|
125
|
+
* The harness's id for THIS tool call, when it publishes one — the first half of
|
|
126
|
+
* a charge record's identity (see `chargeIssueBudget`). Several spellings are
|
|
127
|
+
* accepted because the payload key is the harness's to choose, and a missed id
|
|
128
|
+
* silently degrades to the deterministic command key rather than failing.
|
|
129
|
+
*
|
|
130
|
+
* Kept byte-identical in shape to the refund hook's copy: the two must resolve
|
|
131
|
+
* the same id from the same payload or the refund cannot find the charge.
|
|
132
|
+
*
|
|
133
|
+
* @param {object} input
|
|
134
|
+
* @returns {string|null}
|
|
135
|
+
*/
|
|
136
|
+
function resolveToolCallId(input) {
|
|
137
|
+
for (const value of [input?.tool_use_id, input?.toolUseId, input?.tool_call_id, input?.tool_id]) {
|
|
138
|
+
if (typeof value === 'string' && value.length > 0) return value;
|
|
139
|
+
}
|
|
140
|
+
return null;
|
|
141
|
+
}
|
|
142
|
+
|
|
117
143
|
/**
|
|
118
144
|
* THE CHOICE (#1145) — stated once, so the loop behaviour is explicit rather
|
|
119
145
|
* than emergent.
|
|
@@ -145,7 +171,13 @@ async function resolveSessionId(input, projectDir) {
|
|
|
145
171
|
*
|
|
146
172
|
* NAMED CEILING (BV-004): a loop is detected by `do`/`done` in command position
|
|
147
173
|
* (see `isLoopedIssueCreate`), so an UNROLLED bulk create — 50 create statements
|
|
148
|
-
* chained with `&&` — is not a "loop"
|
|
174
|
+
* chained with `&&` — is not a "loop". Since #1163 it is charged 50, once per
|
|
175
|
+
* issue-create STATEMENT: until then this comment CLAIMED that behaviour while
|
|
176
|
+
* the code called `chargeIssueBudget` exactly once per Bash tool call with the
|
|
177
|
+
* whole command string, so `glab issue create --title A && glab issue create
|
|
178
|
+
* --title B` charged 1 for 2 (measured 2026-09-09). The claim is now true
|
|
179
|
+
* because `findIssueCreateStatements` supplies the per-statement units and the
|
|
180
|
+
* exemption is classified per statement too. Revisit this
|
|
149
181
|
* choice if the overflow triage of a per-session counter file
|
|
150
182
|
* (`.orchestrator/runtime/issue-budget/<hash>.json`) shows operators routinely
|
|
151
183
|
* hitting this deny on loops over a KNOWN literal word list; the cheap answer
|
|
@@ -176,6 +208,73 @@ function formatLoopDenyReason(config) {
|
|
|
176
208
|
].join('\n');
|
|
177
209
|
}
|
|
178
210
|
|
|
211
|
+
/**
|
|
212
|
+
* Park every chargeable statement of a command that does NOT fit under the cap,
|
|
213
|
+
* and return a `formatBlockReason`-shaped verdict for the deny envelope.
|
|
214
|
+
*
|
|
215
|
+
* ## Why this is not `chargeIssueBudget`
|
|
216
|
+
*
|
|
217
|
+
* `chargeIssueBudget` decides ONE creation against the current count, and its
|
|
218
|
+
* strict branch parks only when the count is ALREADY at the cap. A chain of
|
|
219
|
+
* statements that straddles the cap (count 11, max 12, three creates) has no
|
|
220
|
+
* single call shape in that API: the first statement would be ALLOWED and
|
|
221
|
+
* counted, and the deny that follows would leave that count standing for an
|
|
222
|
+
* issue nobody created. So the fit is judged for the chain as a whole and the
|
|
223
|
+
* whole chain is parked — count and exempt untouched, because nothing ran.
|
|
224
|
+
*
|
|
225
|
+
* The exemption CLASSIFICATION still comes from the shared core
|
|
226
|
+
* (`classifyExemption`, applied by the caller); what is local here is only the
|
|
227
|
+
* bookkeeping write, through the module's own public `writeBudgetState`.
|
|
228
|
+
*
|
|
229
|
+
* An identity-less invocation (no session key) must not write at all — the
|
|
230
|
+
* legacy flat path is shared across sessions and writing it would reset a live
|
|
231
|
+
* session's count and drop its parked overflow. Same rule `chargeIssueBudget`'s
|
|
232
|
+
* `persist` applies; the deny still happens, only unrecorded.
|
|
233
|
+
*
|
|
234
|
+
* @param {{ projectDir: string, sessionId: string|null,
|
|
235
|
+
* state: { count: number, exempt: number, overflow: object[], sessionId: string|null },
|
|
236
|
+
* chargeable: Array<{ text: string, title: string|null, description: string|null,
|
|
237
|
+
* descriptionFile: string|null, repo: string|null,
|
|
238
|
+
* cwdChanged: boolean }>,
|
|
239
|
+
* cwd?: string|null,
|
|
240
|
+
* config: { "max-per-session": number, mode: string, overflow: string },
|
|
241
|
+
* now?: string }} opts
|
|
242
|
+
* @returns {{ count: number, max: number, overflowPath: string,
|
|
243
|
+
* overflowSink: string, overflowCount: number }}
|
|
244
|
+
*/
|
|
245
|
+
function parkOverflow({
|
|
246
|
+
projectDir,
|
|
247
|
+
sessionId,
|
|
248
|
+
state,
|
|
249
|
+
chargeable,
|
|
250
|
+
config,
|
|
251
|
+
cwd = null,
|
|
252
|
+
now = new Date().toISOString(),
|
|
253
|
+
}) {
|
|
254
|
+
state.sessionId = sessionId;
|
|
255
|
+
for (const s of chargeable) {
|
|
256
|
+
state.overflow.push(buildOverflowRecord({
|
|
257
|
+
repoRoot: projectDir,
|
|
258
|
+
title: s.title,
|
|
259
|
+
description: s.description,
|
|
260
|
+
descriptionFile: s.descriptionFile,
|
|
261
|
+
repo: s.repo,
|
|
262
|
+
cwd,
|
|
263
|
+
cwdChanged: s.cwdChanged,
|
|
264
|
+
command: s.text,
|
|
265
|
+
at: now,
|
|
266
|
+
}));
|
|
267
|
+
}
|
|
268
|
+
if (sessionId !== null) writeBudgetState(projectDir, state);
|
|
269
|
+
return {
|
|
270
|
+
count: state.count,
|
|
271
|
+
max: config['max-per-session'],
|
|
272
|
+
overflowPath: budgetStatePath(projectDir, sessionId),
|
|
273
|
+
overflowSink: config.overflow,
|
|
274
|
+
overflowCount: state.overflow.length,
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
|
|
179
278
|
// ---------------------------------------------------------------------------
|
|
180
279
|
// Main
|
|
181
280
|
// ---------------------------------------------------------------------------
|
|
@@ -191,8 +290,11 @@ async function main() {
|
|
|
191
290
|
const command = input?.tool_input?.command;
|
|
192
291
|
if (typeof command !== 'string' || command.length === 0) return emitAllow();
|
|
193
292
|
|
|
194
|
-
// G3 — shared matcher. Only ISSUE creation is capped;
|
|
195
|
-
|
|
293
|
+
// G3 — shared matcher, PER STATEMENT (#1163). Only ISSUE creation is capped;
|
|
294
|
+
// `pr`/`mr` create pass. An empty list is the old `!isIssueCreate(command)`
|
|
295
|
+
// short-circuit, unchanged.
|
|
296
|
+
const statements = findIssueCreateStatements(command);
|
|
297
|
+
if (statements.length === 0) return emitAllow();
|
|
196
298
|
|
|
197
299
|
const projectDir = resolveProjectDir() || process.cwd();
|
|
198
300
|
|
|
@@ -202,9 +304,12 @@ async function main() {
|
|
|
202
304
|
|
|
203
305
|
// G3b — bulk creation whose multiplicity is not computable (#1145). The
|
|
204
306
|
// exemption is asked FIRST, through the same classifier chargeIssueBudget
|
|
205
|
-
// uses, so a looped carryover sweep keeps its unconditional pass.
|
|
307
|
+
// uses, so a looped carryover sweep keeps its unconditional pass. It is asked
|
|
308
|
+
// on the FIRST issue-create statement's text, which is the very statement
|
|
309
|
+
// `isLoopedIssueCreate` judges — classifying it on the whole command would
|
|
310
|
+
// let an exempt NEIGHBOUR statement lift the loop deny.
|
|
206
311
|
const uncountableBulk =
|
|
207
|
-
isLoopedIssueCreate(command) && !classifyExemption(
|
|
312
|
+
isLoopedIssueCreate(command) && !classifyExemption(statements[0].text).exempt;
|
|
208
313
|
if (uncountableBulk && config.mode === 'strict') {
|
|
209
314
|
// Nothing is charged and nothing is parked — the command is handed back
|
|
210
315
|
// whole, which is what makes unrolling it the correct next action.
|
|
@@ -213,26 +318,63 @@ async function main() {
|
|
|
213
318
|
|
|
214
319
|
const sessionId = await resolveSessionId(input, projectDir);
|
|
215
320
|
|
|
216
|
-
// G5
|
|
217
|
-
// the
|
|
218
|
-
//
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
321
|
+
// G5 pre-flight — a Bash call is ATOMIC from this hook's point of view: a
|
|
322
|
+
// deny refuses the WHOLE command, so not one of its statements runs. Charging
|
|
323
|
+
// statement-by-statement until one blocks would therefore count creations
|
|
324
|
+
// that never happened (and double-count them when the operator re-issues the
|
|
325
|
+
// command unrolled). So the fit is decided BEFORE any charge, and a command
|
|
326
|
+
// that does not fit parks every chargeable statement without counting any.
|
|
327
|
+
const chargeable = statements.filter((s) => !classifyExemption(s.text).exempt);
|
|
328
|
+
if (config.mode === 'strict' && chargeable.length > 0) {
|
|
329
|
+
const state = readBudgetState(projectDir, sessionId);
|
|
330
|
+
if (state.count + chargeable.length > config['max-per-session']) {
|
|
331
|
+
return emitDeny(formatBlockReason(parkOverflow({
|
|
332
|
+
projectDir, sessionId, state, chargeable, config, cwd: input.cwd,
|
|
333
|
+
})));
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// G6 — charge ONE unit per issue-create STATEMENT. The decision itself stays
|
|
338
|
+
// in the shared core (scripts/lib/issue-budget.mjs), so the programmatic path
|
|
339
|
+
// (scripts/lib/spiral-carryover.mjs runCli) decides identically; what changed
|
|
340
|
+
// in #1163 is only HOW MANY times it is asked. Each statement is judged on
|
|
341
|
+
// its OWN text: `glab issue create --title REAL && glab issue create
|
|
342
|
+
// --label carryover --title X` is 1 charge + 1 exemption, never 2 exemptions.
|
|
343
|
+
// `toolCallId` + the statement index are the CHARGE RECORD's identity (#1347):
|
|
344
|
+
// the refund hook may only give back a slot it can find in `charged[]`, so the
|
|
345
|
+
// charge has to be recorded under the same pair the PostToolUseFailure payload
|
|
346
|
+
// for this very call will present. Absent an id, the deterministic
|
|
347
|
+
// session+command+index key carries it.
|
|
348
|
+
const toolCallId = resolveToolCallId(input);
|
|
349
|
+
const verdicts = statements.map((s, i) =>
|
|
350
|
+
chargeIssueBudget({
|
|
351
|
+
repoRoot: projectDir,
|
|
352
|
+
sessionId,
|
|
353
|
+
command: s.text,
|
|
354
|
+
title: s.title,
|
|
355
|
+
description: s.description,
|
|
356
|
+
descriptionFile: s.descriptionFile,
|
|
357
|
+
repo: s.repo,
|
|
358
|
+
cwd: input.cwd,
|
|
359
|
+
cwdChanged: s.cwdChanged,
|
|
360
|
+
toolCallId,
|
|
361
|
+
statementIndex: i,
|
|
362
|
+
config,
|
|
363
|
+
}),
|
|
364
|
+
);
|
|
365
|
+
const verdict = verdicts[verdicts.length - 1];
|
|
366
|
+
const exemptions = verdicts.filter((v) => v.decision === 'exempt');
|
|
367
|
+
const blocked = verdicts.find((v) => v.decision === 'block');
|
|
226
368
|
|
|
227
|
-
if (
|
|
369
|
+
if (exemptions.length > 0) {
|
|
370
|
+
const reasons = [...new Set(exemptions.map((v) => v.reason))].join(', ');
|
|
228
371
|
process.stderr.write(
|
|
229
|
-
`ℹ pre-bash-issue-budget: exempt (${
|
|
230
|
-
`(${verdict.count}/${verdict.max})\n`,
|
|
372
|
+
`ℹ pre-bash-issue-budget: ${exemptions.length} exempt statement(s) (${reasons}) — ` +
|
|
373
|
+
`cap not charged (${verdict.count}/${verdict.max})\n`,
|
|
231
374
|
);
|
|
232
|
-
return emitAllow();
|
|
233
375
|
}
|
|
234
376
|
|
|
235
|
-
if (
|
|
377
|
+
if (verdicts.some((v) => v.decision === 'warn')) {
|
|
236
378
|
process.stderr.write(
|
|
237
379
|
`⚠ pre-bash-issue-budget: session cap exceeded — ${verdict.count}/${verdict.max} ` +
|
|
238
380
|
`issues created (mode: warn — allowing). Set \`issue-budget.mode: strict\` to enforce.\n`,
|
|
@@ -240,7 +382,9 @@ async function main() {
|
|
|
240
382
|
return emitAllow();
|
|
241
383
|
}
|
|
242
384
|
|
|
243
|
-
if (
|
|
385
|
+
if (exemptions.length === statements.length) return emitAllow();
|
|
386
|
+
|
|
387
|
+
if (blocked) {
|
|
244
388
|
// Single channel (#906). formatBlockReason's multi-line text — overflow
|
|
245
389
|
// store path, the [Backlog-Sammel] fold-in promise, the exemption list and
|
|
246
390
|
// the cap-raising hint — used to go to stderr AND to a duplicated `exit 2`
|
|
@@ -249,7 +393,7 @@ async function main() {
|
|
|
249
393
|
// to Claude (the actor that must re-file or defer the issue), while the
|
|
250
394
|
// operator gets the first line as the systemMessage headline. Under exit 0
|
|
251
395
|
// a stderr write would only reach the debug log — dead, but alive-looking.
|
|
252
|
-
emitDeny(formatBlockReason(
|
|
396
|
+
return emitDeny(formatBlockReason(blocked));
|
|
253
397
|
}
|
|
254
398
|
|
|
255
399
|
// A PERMITTED bulk create is charged ONCE, which is an undercount by
|
|
@@ -64,6 +64,33 @@
|
|
|
64
64
|
* `subagent_transcript_found: true` as token-bearing; summing across the
|
|
65
65
|
* history double-counts the parent once per subagent.
|
|
66
66
|
*
|
|
67
|
+
* TOKEN-DATA PROVENANCE — SERIES BREAK 2026-09-09 (schema_version 2, #1244).
|
|
68
|
+
* Until this fix `token_input` was the raw `usage.input_tokens` sum only, and
|
|
69
|
+
* `cache_read_input_tokens` / `cache_creation_input_tokens` were read nowhere in
|
|
70
|
+
* the repo. Under prompt caching virtually the whole prompt is cache traffic, so
|
|
71
|
+
* that number described a rounding error rather than the run: measured
|
|
72
|
+
* 2026-09-09 on agent af50d1eda37099e17, the ledger recorded token_input 56
|
|
73
|
+
* where its transcript holds 56 + 3,528,536 cache_read + 147,587 cache_creation
|
|
74
|
+
* = 3,676,179 — an understatement of 65,646×; session-wide 1,518 vs 85,271,214.
|
|
75
|
+
* (token_output was within ~11% and is unaffected.)
|
|
76
|
+
*
|
|
77
|
+
* From schema_version 2 onward:
|
|
78
|
+
* token_input = uncached + cache_read + cache_creation
|
|
79
|
+
* (BILLABLE PROMPT VOLUME — a redefinition, not a
|
|
80
|
+
* widening: v1 and v2 values are not comparable)
|
|
81
|
+
* token_input_uncached = raw usage.input_tokens (additive)
|
|
82
|
+
* token_cache_read = usage.cache_read_input_tokens (additive)
|
|
83
|
+
* token_cache_creation = usage.cache_creation_input_tokens (additive)
|
|
84
|
+
* model = message.model, or null (additive; enables pricing)
|
|
85
|
+
* gen_ai.usage.input_tokens stays the RAW UNCACHED value (OTel semantic), so
|
|
86
|
+
* it is deliberately ≠ token_input under v2. Two new OTel aliases carry the
|
|
87
|
+
* cache buckets.
|
|
88
|
+
*
|
|
89
|
+
* FORWARD-ONLY. Nothing recomputes the v1 history, so consumers MUST gate any
|
|
90
|
+
* v2 sum on `schema_version >= 2` — `scripts/lib/session-token-rollup.mjs` does
|
|
91
|
+
* exactly that and reports the excluded v1 records as `legacy_v1_records`
|
|
92
|
+
* rather than silently folding them in.
|
|
93
|
+
*
|
|
67
94
|
* A SECOND, independent defect rode along until #950: the requestId dedup kept
|
|
68
95
|
* the FIRST usage block per id, which on a streaming transcript is a partial
|
|
69
96
|
* snapshot (typically `output_tokens: 1`). Any record written before #950 —
|
|
@@ -391,13 +418,27 @@ function readStdinJson() {
|
|
|
391
418
|
* so the present side still sums and the aggregate stays valid.
|
|
392
419
|
*
|
|
393
420
|
* NEVER throws. Any failure (missing/unreadable path, 0 assistant turns, parse
|
|
394
|
-
* error) yields
|
|
421
|
+
* error) yields an all-null result so the hook still exits 0.
|
|
422
|
+
*
|
|
423
|
+
* Four buckets, not two (#1244 / schema_version 2): `input_tokens`,
|
|
424
|
+
* `cache_read_input_tokens`, `cache_creation_input_tokens` and `output_tokens`
|
|
425
|
+
* are accumulated SEPARATELY per deduped turn, because they are billed at three
|
|
426
|
+
* different rates. `model` is captured from `message.model` of the last kept
|
|
427
|
+
* block that carries one (null when absent) — it is what makes a cost estimate
|
|
428
|
+
* possible at all downstream.
|
|
395
429
|
*
|
|
396
430
|
* @param {string|undefined|null} transcriptPath — absolute path from stdin
|
|
397
|
-
* @returns {{
|
|
431
|
+
* @returns {{ tokenInputUncached: number|null, tokenCacheRead: number|null,
|
|
432
|
+
* tokenCacheCreation: number|null, tokenOutput: number|null, model: string|null }}
|
|
398
433
|
*/
|
|
399
434
|
function extractTranscriptUsage(transcriptPath) {
|
|
400
|
-
const nullResult = {
|
|
435
|
+
const nullResult = {
|
|
436
|
+
tokenInputUncached: null,
|
|
437
|
+
tokenCacheRead: null,
|
|
438
|
+
tokenCacheCreation: null,
|
|
439
|
+
tokenOutput: null,
|
|
440
|
+
model: null,
|
|
441
|
+
};
|
|
401
442
|
try {
|
|
402
443
|
if (typeof transcriptPath !== 'string' || !transcriptPath.trim()) return nullResult;
|
|
403
444
|
if (!fs.existsSync(transcriptPath)) return nullResult;
|
|
@@ -447,9 +488,15 @@ function extractTranscriptUsage(transcriptPath) {
|
|
|
447
488
|
// Dedup by requestId — keep the LAST usage block per id (#950). The
|
|
448
489
|
// repeats are cumulative streaming snapshots, so overwriting is what
|
|
449
490
|
// promotes the partial first snapshot to the response's real total.
|
|
491
|
+
// Model id rides along with the usage block (#1244): it is per-turn data
|
|
492
|
+
// and the only thing that makes the record priceable downstream.
|
|
493
|
+
const turnModel = typeof obj.message?.model === 'string' && obj.message.model
|
|
494
|
+
? obj.message.model
|
|
495
|
+
: null;
|
|
496
|
+
|
|
450
497
|
const requestId = obj.requestId;
|
|
451
498
|
if (typeof requestId === 'string' && requestId) {
|
|
452
|
-
byRequestId.set(requestId, usage);
|
|
499
|
+
byRequestId.set(requestId, { usage, model: turnModel });
|
|
453
500
|
continue;
|
|
454
501
|
}
|
|
455
502
|
|
|
@@ -461,9 +508,9 @@ function extractTranscriptUsage(transcriptPath) {
|
|
|
461
508
|
// usable identity; anything else falls through to the individual count.
|
|
462
509
|
const messageId = obj.message?.id;
|
|
463
510
|
if (typeof messageId === 'string' && messageId) {
|
|
464
|
-
byMessageId.set(messageId, usage);
|
|
511
|
+
byMessageId.set(messageId, { usage, model: turnModel });
|
|
465
512
|
} else {
|
|
466
|
-
unkeyable.push(usage);
|
|
513
|
+
unkeyable.push({ usage, model: turnModel });
|
|
467
514
|
}
|
|
468
515
|
}
|
|
469
516
|
|
|
@@ -472,23 +519,34 @@ function extractTranscriptUsage(transcriptPath) {
|
|
|
472
519
|
// No assistant turns with usage → leave fields null (forward-compat).
|
|
473
520
|
if (kept.length === 0) return nullResult;
|
|
474
521
|
|
|
475
|
-
let
|
|
522
|
+
let tokenInputUncached = 0;
|
|
523
|
+
let tokenCacheRead = 0;
|
|
524
|
+
let tokenCacheCreation = 0;
|
|
476
525
|
let tokenOutput = 0;
|
|
477
|
-
|
|
526
|
+
let model = null;
|
|
527
|
+
for (const { usage, model: turnModel } of kept) {
|
|
478
528
|
// Per-turn clamp (#624): add a turn's value ONLY when it is a non-negative
|
|
479
529
|
// integer. A poisoned value (negative, NaN, float like 10.5) is skipped so
|
|
480
530
|
// the good turns survive. An absent side contributes 0, not null.
|
|
481
531
|
const inTok = usage.input_tokens;
|
|
532
|
+
const cacheRead = usage.cache_read_input_tokens;
|
|
533
|
+
const cacheCreation = usage.cache_creation_input_tokens;
|
|
482
534
|
const outTok = usage.output_tokens;
|
|
483
|
-
if (Number.isInteger(inTok) && inTok >= 0)
|
|
535
|
+
if (Number.isInteger(inTok) && inTok >= 0) tokenInputUncached += inTok;
|
|
536
|
+
if (Number.isInteger(cacheRead) && cacheRead >= 0) tokenCacheRead += cacheRead;
|
|
537
|
+
if (Number.isInteger(cacheCreation) && cacheCreation >= 0) tokenCacheCreation += cacheCreation;
|
|
484
538
|
if (Number.isInteger(outTok) && outTok >= 0) tokenOutput += outTok;
|
|
539
|
+
if (turnModel !== null) model = turnModel;
|
|
485
540
|
}
|
|
486
541
|
|
|
487
|
-
// The
|
|
488
|
-
// above (Σ of non-negative integers), so emit
|
|
542
|
+
// The aggregates are guaranteed non-negative integers by per-turn clamping
|
|
543
|
+
// above (Σ of non-negative integers), so emit them directly.
|
|
489
544
|
return {
|
|
490
|
-
|
|
545
|
+
tokenInputUncached,
|
|
546
|
+
tokenCacheRead,
|
|
547
|
+
tokenCacheCreation,
|
|
491
548
|
tokenOutput,
|
|
549
|
+
model,
|
|
492
550
|
};
|
|
493
551
|
} catch {
|
|
494
552
|
return nullResult;
|
|
@@ -657,7 +715,7 @@ async function main() {
|
|
|
657
715
|
timestamp: new Date().toISOString(),
|
|
658
716
|
event,
|
|
659
717
|
agent_id: agentId,
|
|
660
|
-
schema_version:
|
|
718
|
+
schema_version: 2,
|
|
661
719
|
...(agentType !== null ? { agent_type: agentType } : {}),
|
|
662
720
|
...(parentSessionId !== null ? { parent_session_id: parentSessionId } : {}),
|
|
663
721
|
};
|
|
@@ -705,23 +763,51 @@ async function main() {
|
|
|
705
763
|
// NO fallback to input.transcript_path: that path is the parent session
|
|
706
764
|
// transcript, and reading it is the #949 defect (every stop inherited the
|
|
707
765
|
// parent's running totals). A phantom stop gets null — the honest value.
|
|
708
|
-
const {
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
766
|
+
const { tokenInputUncached, tokenCacheRead, tokenCacheCreation, tokenOutput, model } =
|
|
767
|
+
subagentTranscriptFound
|
|
768
|
+
? extractTranscriptUsage(subagentTranscriptPath)
|
|
769
|
+
: {
|
|
770
|
+
tokenInputUncached: null,
|
|
771
|
+
tokenCacheRead: null,
|
|
772
|
+
tokenCacheCreation: null,
|
|
773
|
+
tokenOutput: null,
|
|
774
|
+
model: null,
|
|
775
|
+
};
|
|
776
|
+
|
|
777
|
+
// schema_version 2 (#1244): `token_input` is now BILLABLE PROMPT VOLUME —
|
|
778
|
+
// uncached + cache_read + cache_creation — and the three components are
|
|
779
|
+
// written additively beside it. See the file header § TOKEN-DATA PROVENANCE
|
|
780
|
+
// for the 2026-09-09 series break this creates.
|
|
781
|
+
if (tokenInputUncached !== null) {
|
|
782
|
+
record.token_input =
|
|
783
|
+
tokenInputUncached + (tokenCacheRead ?? 0) + (tokenCacheCreation ?? 0);
|
|
784
|
+
record.token_input_uncached = tokenInputUncached;
|
|
785
|
+
record.token_cache_read = tokenCacheRead;
|
|
786
|
+
record.token_cache_creation = tokenCacheCreation;
|
|
787
|
+
}
|
|
712
788
|
if (tokenOutput !== null) record.token_output = tokenOutput;
|
|
713
789
|
|
|
790
|
+
// Model id (#1244) — null when the transcript exposes none. Cost is NOT
|
|
791
|
+
// computed here: pricing lives in scripts/lib/telemetry/pricing.mjs and is
|
|
792
|
+
// applied by the session rollup, so this hot-path hook keeps its import
|
|
793
|
+
// graph unchanged.
|
|
794
|
+
record.model = model;
|
|
795
|
+
|
|
714
796
|
// Cost is best-effort / forward-compat (#624): the native transcript does
|
|
715
797
|
// NOT expose total_cost_usd today, so this is null in practice. No rate
|
|
716
|
-
// table — use the native cost only, default null when absent.
|
|
798
|
+
// table is applied HERE — use the native cost only, default null when absent.
|
|
717
799
|
const totalCostUsd =
|
|
718
800
|
typeof input.total_cost_usd === 'number' && Number.isFinite(input.total_cost_usd) && input.total_cost_usd >= 0
|
|
719
801
|
? input.total_cost_usd
|
|
720
802
|
: null;
|
|
721
803
|
record.total_cost_usd = totalCostUsd;
|
|
722
804
|
|
|
723
|
-
// OTel alias — #411 additive
|
|
724
|
-
|
|
805
|
+
// OTel alias — #411 additive. `gen_ai.usage.input_tokens` stays the RAW
|
|
806
|
+
// UNCACHED value (OTel semantic), which is why it is deliberately NOT equal
|
|
807
|
+
// to `token_input` under schema_version 2.
|
|
808
|
+
record['gen_ai.usage.input_tokens'] = tokenInputUncached;
|
|
809
|
+
record['gen_ai.usage.cache_read_input_tokens'] = tokenCacheRead;
|
|
810
|
+
record['gen_ai.usage.cache_creation_input_tokens'] = tokenCacheCreation;
|
|
725
811
|
record['gen_ai.usage.output_tokens'] = tokenOutput;
|
|
726
812
|
record['gen_ai.system'] = 'anthropic';
|
|
727
813
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "session-orchestrator",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "5.0.0",
|
|
4
4
|
"description": "Loop engineering for AI coding agents — turn ad-hoc sessions into a repeatable research → plan → wave-execute → close loop with verification gates. Runs on Claude Code, Codex CLI, Cursor, and Pi.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"homepage": "https://session-orchestrator.com",
|
|
@@ -36,6 +36,7 @@
|
|
|
36
36
|
"output-styles/",
|
|
37
37
|
"monitors/",
|
|
38
38
|
"assets/",
|
|
39
|
+
"!assets/marketing/**",
|
|
39
40
|
".claude-plugin/",
|
|
40
41
|
".codex-plugin/",
|
|
41
42
|
".cursor-plugin/",
|
|
@@ -69,7 +70,7 @@
|
|
|
69
70
|
"dependencies": {
|
|
70
71
|
"@babel/parser": "^7.26.0",
|
|
71
72
|
"ajv": "^8.17.1",
|
|
72
|
-
"js-yaml": "^4.3.
|
|
73
|
+
"js-yaml": "^4.3.2",
|
|
73
74
|
"mdast-util-to-string": "^4.0.0",
|
|
74
75
|
"picomatch": "^4.0.4",
|
|
75
76
|
"remark": "^15.0.0",
|
|
@@ -83,12 +84,12 @@
|
|
|
83
84
|
"@commitlint/cli": "^19.6.0",
|
|
84
85
|
"@commitlint/config-conventional": "^19.6.0",
|
|
85
86
|
"@eslint/js": "^10.0.1",
|
|
86
|
-
"@vitest/coverage-v8": "^4.1.
|
|
87
|
+
"@vitest/coverage-v8": "^4.1.11",
|
|
87
88
|
"eslint": "^10.2.1",
|
|
88
89
|
"husky": "^9.1.7",
|
|
89
90
|
"lint-staged": "^15.2.10",
|
|
90
91
|
"prettier": "^3.0.0",
|
|
91
|
-
"vitest": "^4.1.
|
|
92
|
+
"vitest": "^4.1.11"
|
|
92
93
|
},
|
|
93
94
|
"overrides": {
|
|
94
95
|
"vite": "^8.0.16",
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Grill a running web app's UX — a deterministic mechanical pass (axe, target size, overflow, journeys) followed by a screenshot-grounded interrogation of the operator.
|
|
3
|
+
argument-hint: "[url | manifest-path]"
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# /ux-grill
|
|
7
|
+
|
|
8
|
+
Use the Session Orchestrator command definition at `commands/ux-grill.md`.
|
|
9
|
+
|
|
10
|
+
Arguments: $@
|
|
11
|
+
|
|
12
|
+
Read that command file and follow it exactly. When it references `$ARGUMENTS`, substitute the arguments above. Keep all Session Orchestrator platform fallbacks intact.
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/** Data-only lookup for the configured local bootstrap baseline. */
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { realpathSync } from 'node:fs';
|
|
5
|
+
import { pathToFileURL } from 'node:url';
|
|
6
|
+
import { loadBaselineArchetypes } from './lib/baseline-archetypes.mjs';
|
|
7
|
+
|
|
8
|
+
export async function main(argv = process.argv.slice(2)) {
|
|
9
|
+
const options = {};
|
|
10
|
+
for (let index = 0; index < argv.length; index++) {
|
|
11
|
+
const arg = argv[index];
|
|
12
|
+
if (arg === '--help' || arg === '-h') {
|
|
13
|
+
process.stdout.write('Usage: node scripts/baseline-archetypes.mjs [--repo PATH] [--archetype ID]\nOffline, read-only JSON lookup. Exit 0: public/private; exit 2: invalid configuration or contract.\n');
|
|
14
|
+
return 0;
|
|
15
|
+
}
|
|
16
|
+
if (!['--repo', '--archetype'].includes(arg) || !argv[index + 1] || argv[index + 1].startsWith('--')) {
|
|
17
|
+
process.stdout.write(`${JSON.stringify({ status: 'error', reason: 'invalid-arguments', archetypes: [], selected: null })}\n`);
|
|
18
|
+
return 2;
|
|
19
|
+
}
|
|
20
|
+
options[arg === '--repo' ? 'repoRoot' : 'archetype'] = argv[++index];
|
|
21
|
+
}
|
|
22
|
+
if (options.repoRoot) options.repoRoot = path.resolve(options.repoRoot);
|
|
23
|
+
const result = await loadBaselineArchetypes(options);
|
|
24
|
+
process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
|
|
25
|
+
return result.status === 'error' ? 2 : 0;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (process.argv[1] && import.meta.url === pathToFileURL(realpathSync(process.argv[1])).href) process.exitCode = await main();
|
|
@@ -258,8 +258,10 @@ if (isMain) {
|
|
|
258
258
|
const inFlight = inFlightFilesFromLogFile(logPath);
|
|
259
259
|
if (inFlight.length > 0) {
|
|
260
260
|
console.error(
|
|
261
|
-
`[ci]
|
|
262
|
-
`(
|
|
261
|
+
`[ci] KILL ARTEFACT — the shard timed out or was killed before vitest wrote its ` +
|
|
262
|
+
`result file (NOT a test failure). ${inFlight.length} file(s) were merely in-flight ` +
|
|
263
|
+
`at the kill: ${inFlight.join(', ')}. Re-run the shard; if it repeats, check runner ` +
|
|
264
|
+
`load/sharding before suspecting these files.`,
|
|
263
265
|
);
|
|
264
266
|
}
|
|
265
267
|
}
|
|
@@ -182,13 +182,21 @@ function countContentLines(body) {
|
|
|
182
182
|
* dry-run mode, where there is no `mergePeerCard()` stats object to read).
|
|
183
183
|
* Pure function — no I/O.
|
|
184
184
|
*
|
|
185
|
-
*
|
|
186
|
-
*
|
|
187
|
-
*
|
|
188
|
-
*
|
|
189
|
-
*
|
|
190
|
-
*
|
|
191
|
-
*
|
|
185
|
+
* Resolution order:
|
|
186
|
+
* 1. non-string or empty body → 0 (the response never touched the target);
|
|
187
|
+
* 2. `<!-- BEGIN MANAGED: <name> -->` sentinels present (the grammar
|
|
188
|
+
* `scripts/lib/peer-cards/merger.mjs` parses) → their count;
|
|
189
|
+
* 3. otherwise the count of level-2 `## ` headings — the deriver's actual
|
|
190
|
+
* output contract per {@link buildPrompt} is a bare full-body
|
|
191
|
+
* replacement structured by `## ` headings, not sentinels (#1319: the
|
|
192
|
+
* old sentinel-or-1 rule reported 1 for a 12-section body). Headings
|
|
193
|
+
* inside ``` / ~~~ code fences are NOT counted;
|
|
194
|
+
* 4. a non-empty body with neither → 1 (a full-body replacement is still a
|
|
195
|
+
* real change to the target).
|
|
196
|
+
*
|
|
197
|
+
* Dry-run only. Apply mode counts `mergePeerCard()` stats instead
|
|
198
|
+
* (`replaced + appended`, i.e. merged sentinel sections) — the two modes
|
|
199
|
+
* measure different quantities on the same event field.
|
|
192
200
|
*
|
|
193
201
|
* @param {string | undefined} diffText — `diff.user` or `diff.agent` from
|
|
194
202
|
* {@link parseResponse}; `undefined` when the target was not emitted.
|
|
@@ -197,7 +205,23 @@ function countContentLines(body) {
|
|
|
197
205
|
export function countManagedSections(diffText) {
|
|
198
206
|
if (typeof diffText !== 'string' || diffText.length === 0) return 0;
|
|
199
207
|
const matches = diffText.match(/<!--\s*BEGIN\s+MANAGED:\s*[\w-]+\s*-->/g);
|
|
200
|
-
|
|
208
|
+
if (matches && matches.length > 0) return matches.length;
|
|
209
|
+
let headings = 0;
|
|
210
|
+
let fence = null; // { char, len } while inside a code fence
|
|
211
|
+
for (const line of diffText.split(/\r?\n/)) {
|
|
212
|
+
const f = /^ {0,3}(`{3,}|~{3,})(.*)$/.exec(line);
|
|
213
|
+
if (fence) {
|
|
214
|
+
// CommonMark close: same char, at least as long, no info string.
|
|
215
|
+
if (f && f[1][0] === fence.char && f[1].length >= fence.len && f[2].trim() === '') fence = null;
|
|
216
|
+
continue;
|
|
217
|
+
}
|
|
218
|
+
if (f) {
|
|
219
|
+
fence = { char: f[1][0], len: f[1].length };
|
|
220
|
+
continue;
|
|
221
|
+
}
|
|
222
|
+
if (/^## /.test(line)) headings += 1;
|
|
223
|
+
}
|
|
224
|
+
return headings > 0 ? headings : 1;
|
|
201
225
|
}
|
|
202
226
|
|
|
203
227
|
/**
|