session-orchestrator 4.2.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.
Files changed (129) hide show
  1. package/.agents/skills/session-start/SKILL.md +1 -1
  2. package/.agents/skills/ux-grill/SKILL.md +22 -0
  3. package/.claude-plugin/marketplace.json +1 -1
  4. package/.claude-plugin/plugin.json +3 -2
  5. package/.codex-plugin/plugin.json +1 -1
  6. package/.codex-plugin/skills/session-start/SKILL.md +1 -1
  7. package/.codex-plugin/skills/ux-grill/SKILL.md +21 -0
  8. package/.codex-plugin/skills/ux-grill/agents/openai.yaml +5 -0
  9. package/.cursor/commands/ux-grill.md +14 -0
  10. package/.cursor/skills/session-start/SKILL.md +1 -1
  11. package/.cursor/skills/ux-grill/SKILL.md +13 -0
  12. package/.cursor-plugin/plugin.json +1 -1
  13. package/AGENTS.md +2 -1
  14. package/CHANGELOG.md +94 -1
  15. package/README.md +98 -86
  16. package/agents/dialectic-deriver.md +11 -0
  17. package/commands/ux-grill.md +51 -0
  18. package/docs/USER-GUIDE.md +2 -2
  19. package/docs/codex-setup.md +8 -0
  20. package/docs/components.md +7 -7
  21. package/docs/events-schema.md +9 -5
  22. package/docs/github-mirror-protection.md +61 -20
  23. package/docs/migration-v5.md +62 -0
  24. package/docs/scope-collision-guard.md +14 -0
  25. package/hooks/_lib/hook-import-set.json +44 -2
  26. package/hooks/_lib/lock-bootstrap.mjs +84 -1
  27. package/hooks/_lib/vcs-create-matcher.mjs +190 -3
  28. package/hooks/enforce-scope.mjs +201 -0
  29. package/hooks/hooks-codex.json +1 -1
  30. package/hooks/hooks-cursor.json +5 -0
  31. package/hooks/hooks.json +7 -2
  32. package/hooks/on-session-start.mjs +171 -49
  33. package/hooks/post-bash-issue-budget-refund.mjs +375 -0
  34. package/hooks/pre-auq-clarity.mjs +70 -18
  35. package/hooks/pre-bash-issue-budget.mjs +51 -4
  36. package/package.json +2 -1
  37. package/pi/prompts/ux-grill.md +12 -0
  38. package/scripts/ci/assert-vitest-green.mjs +4 -2
  39. package/scripts/dialectic-deriver.mjs +32 -8
  40. package/scripts/emit-session.mjs +72 -1
  41. package/scripts/lib/agent-status.mjs +441 -9
  42. package/scripts/lib/auq/schema.mjs +10 -3
  43. package/scripts/lib/ci-status-banner.mjs +29 -6
  44. package/scripts/lib/claude-md-budget-lint.mjs +52 -2
  45. package/scripts/lib/config.mjs +12 -1
  46. package/scripts/lib/eval/engine.mjs +7 -1
  47. package/scripts/lib/file-lock.mjs +114 -13
  48. package/scripts/lib/git-porcelain.mjs +113 -0
  49. package/scripts/lib/instruction-budget-guard.mjs +415 -47
  50. package/scripts/lib/io.mjs +29 -4
  51. package/scripts/lib/issue-budget.mjs +336 -6
  52. package/scripts/lib/learnings/sizing-subject.mjs +44 -0
  53. package/scripts/lib/locks/staging-fence-lock.mjs +19 -38
  54. package/scripts/lib/locks/state-md-lock.mjs +19 -41
  55. package/scripts/lib/maintenance-due-banner.mjs +11 -1
  56. package/scripts/lib/peer-cards/merger.mjs +143 -0
  57. package/scripts/lib/pre-dispatch-check.mjs +20 -14
  58. package/scripts/lib/project-hygiene.mjs +81 -30
  59. package/scripts/lib/quality-gate.mjs +14 -65
  60. package/scripts/lib/reconcile/engine.mjs +19 -1
  61. package/scripts/lib/reconcile/writer.mjs +278 -11
  62. package/scripts/lib/scope-echo.mjs +346 -0
  63. package/scripts/lib/session-lock.mjs +62 -2
  64. package/scripts/lib/session-record-repair.mjs +91 -0
  65. package/scripts/lib/session-schema/filters.mjs +26 -1
  66. package/scripts/lib/session-start-probes.mjs +419 -53
  67. package/scripts/lib/test-runner/artifact-paths.mjs +30 -5
  68. package/scripts/lib/test-runner/issue-reconcile.mjs +45 -8
  69. package/scripts/lib/tmux-layout/layouts.mjs +62 -4
  70. package/scripts/lib/ux-grill/collect.mjs +1163 -0
  71. package/scripts/lib/ux-grill/compare.mjs +285 -0
  72. package/scripts/lib/ux-grill/manifest.mjs +618 -0
  73. package/scripts/lib/ux-grill/measures.mjs +431 -0
  74. package/scripts/lib/ux-grill/paths.mjs +224 -0
  75. package/scripts/lib/ux-grill/pencil-coverage.mjs +284 -0
  76. package/scripts/lib/ux-grill/reconcile.mjs +344 -0
  77. package/scripts/lib/ux-grill/run-record.mjs +316 -0
  78. package/scripts/lib/ux-grill/schema.mjs +321 -0
  79. package/scripts/lib/validate/check-untracked-test-deps.mjs +33 -19
  80. package/scripts/lib/validate/check-unwired-features.mjs +48 -20
  81. package/scripts/lib/vault-status/board-lock.mjs +18 -0
  82. package/scripts/lib/vault-status/board-writer.mjs +8 -0
  83. package/scripts/mcp-server.sh +16 -1
  84. package/scripts/release.mjs +7 -2
  85. package/skills/bootstrap/SKILL.md +12 -209
  86. package/skills/bootstrap/references/bootstrap-ecosystem-health-flow.md +48 -0
  87. package/skills/bootstrap/references/bootstrap-refresh-lock-flow.md +37 -0
  88. package/skills/bootstrap/references/bootstrap-retroactive-flow.md +108 -0
  89. package/skills/bootstrap/references/bootstrap-rules-fetch-bridge.md +64 -0
  90. package/skills/claude-md-drift-check/SKILL.md +9 -2
  91. package/skills/claude-md-drift-check/checker.mjs +213 -21
  92. package/skills/discovery/SKILL.md +6 -173
  93. package/skills/discovery/probes/vault-staleness.mjs +35 -5
  94. package/skills/discovery/probes-docs.md +8 -4
  95. package/skills/discovery/probes-supply-chain.md +4 -2
  96. package/skills/discovery/probes-ui.md +7 -3
  97. package/skills/discovery/probes-vault.md +12 -4
  98. package/skills/discovery/references/discovery-interactive-triage.md +139 -0
  99. package/skills/discovery/references/discovery-triage-state.md +54 -0
  100. package/skills/eval/rubric-v1.md +13 -0
  101. package/skills/evolve/SKILL.md +2 -458
  102. package/skills/evolve/references/evolve-analyze-mode.md +360 -0
  103. package/skills/evolve/references/evolve-dialectic-mode.md +139 -0
  104. package/skills/plan/mode-retro.md +4 -3
  105. package/skills/reconcile/SKILL.md +10 -0
  106. package/skills/session-end/drift-operations.md +20 -5
  107. package/skills/session-end/metrics-collection.md +1 -0
  108. package/skills/session-end/phase-3-6-tail.md +4 -2
  109. package/skills/session-end/references/phase-2-quality-gate.md +3 -3
  110. package/skills/session-end/references/phase-5-issue-cleanup.md +6 -1
  111. package/skills/session-end/session-metrics-write.md +2 -0
  112. package/skills/session-plan/SKILL.md +2 -144
  113. package/skills/session-plan/references/session-plan-task-classification.md +152 -0
  114. package/skills/session-start/SKILL.md +24 -6
  115. package/skills/session-start/references/operations-contract.md +114 -0
  116. package/skills/session-start/references/phase-4-ssot-environment-check.md +22 -20
  117. package/skills/session-start/soul.md +2 -2
  118. package/skills/test-runner/SKILL.md +1 -1
  119. package/skills/tmux-layout/SKILL.md +3 -1
  120. package/skills/ux-grill/SKILL.md +211 -0
  121. package/skills/ux-grill/rubric-v2.md +201 -0
  122. package/skills/ux-grill/soul.md +76 -0
  123. package/skills/wave-executor/SKILL.md +3 -128
  124. package/skills/wave-executor/references/wave-executor-quality-gate.md +61 -0
  125. package/skills/wave-executor/references/wave-executor-state-init.md +86 -0
  126. package/skills/wave-executor/references/wave-loop-dispatch.md +8 -0
  127. package/skills/wave-executor/references/wave-loop-review.md +18 -5
  128. package/templates/_shared/ux-manifest.template.md +149 -0
  129. package/templates/_shared/journey-manifest.md +0 -114
@@ -4,6 +4,8 @@
4
4
  * All functions are side-effect free. Callers are responsible for mkdir.
5
5
  *
6
6
  * Exports:
7
+ * RUN_ID_PATTERN → RegExp accepted run-id shape
8
+ * assertRunId(runId) → string the runId, or throws TypeError
7
9
  * makeRunId() → string e.g., '12345-1715688000123'
8
10
  * runDirPath(runId) → string
9
11
  * findingsPath(runId) → string
@@ -19,6 +21,30 @@ import path from 'node:path';
19
21
  const ROLLUP_REL = '.orchestrator/metrics/test-runs.jsonl';
20
22
  const TEST_RUNS_REL = '.orchestrator/metrics/test-runs';
21
23
 
24
+ /**
25
+ * Accepted shape of a run id — the single definition for test-runner AND
26
+ * ux-grill (`scripts/lib/ux-grill/paths.mjs` imports it; ux-grill → test-runner
27
+ * is the allowed dependency direction). A run id reaches `path.join` unescaped,
28
+ * so `/` would escape the run directory. `.` and `..` match this class too and
29
+ * are rejected separately by {@link assertRunId}.
30
+ * @type {RegExp}
31
+ */
32
+ export const RUN_ID_PATTERN = /^[A-Za-z0-9._-]+$/;
33
+
34
+ /**
35
+ * Validate a run id: a string matching {@link RUN_ID_PATTERN} that is not `.`
36
+ * or `..` (both resolve to the base dir or its parent — path traversal, #1330).
37
+ * @param {unknown} runId
38
+ * @returns {string} the same runId
39
+ * @throws {TypeError} if runId is not a string, does not match the pattern, or is `.`/`..`
40
+ */
41
+ export function assertRunId(runId) {
42
+ if (typeof runId !== 'string' || !RUN_ID_PATTERN.test(runId) || runId === '.' || runId === '..') {
43
+ throw new TypeError(`runId must match ${RUN_ID_PATTERN} and not be "." or "..", got ${JSON.stringify(runId)}`);
44
+ }
45
+ return runId;
46
+ }
47
+
22
48
  /**
23
49
  * Generate a unique run-id from PID + millisecond timestamp.
24
50
  * Format: `${process.pid}-${Date.now()}`
@@ -29,15 +55,14 @@ export function makeRunId() {
29
55
  }
30
56
 
31
57
  /**
32
- * Construct the run-dir path for a given runId.
58
+ * Construct the run-dir path for a given runId. Every other per-run builder in
59
+ * this module routes through here, so this is the single path-traversal guard.
33
60
  * @param {string} runId
34
61
  * @returns {string}
62
+ * @throws {TypeError} on an invalid runId (see {@link assertRunId})
35
63
  */
36
64
  export function runDirPath(runId) {
37
- if (typeof runId !== 'string' || runId.length === 0) {
38
- throw new TypeError('runDirPath: runId must be a non-empty string');
39
- }
40
- return path.join(TEST_RUNS_REL, runId);
65
+ return path.join(TEST_RUNS_REL, assertRunId(runId));
41
66
  }
42
67
 
43
68
  /**
@@ -148,6 +148,21 @@ function sanitizeRecommendation(text) {
148
148
  return text.replace(/\*\*Fingerprint:\*\*/gi, '__Fingerprint__');
149
149
  }
150
150
 
151
+ /**
152
+ * Render untrusted text as the content of ONE inline Markdown code span:
153
+ * sentinel literals neutralised (sanitizeRecommendation), line breaks and NUL
154
+ * folded to a space, backticks turned into `'` — so a page-controlled locator
155
+ * can neither close its code span nor start a new body line.
156
+ *
157
+ * @param {unknown} text
158
+ * @returns {string}
159
+ */
160
+ function codeSpanContent(text) {
161
+ return sanitizeRecommendation(String(text ?? ''))
162
+ .replace(/[\r\n\0]+/g, ' ')
163
+ .replace(/`/g, "'");
164
+ }
165
+
151
166
  // ---------------------------------------------------------------------------
152
167
  // Body-length validation (#389 SEC-IR-LOW-1)
153
168
  // ---------------------------------------------------------------------------
@@ -182,25 +197,31 @@ function checkBodyLength(body) {
182
197
  * Newlines within the body are intentional and safe — execFile passes
183
198
  * --description as a single argv element, not through a shell.
184
199
  *
185
- * Applies sanitizeRecommendation() to the recommendation field (#388) before
186
- * embedding it, so the authoritative `**Fingerprint:** \`<fp>\`` sentinel
187
- * line cannot be spoofed by attacker-controlled recommendation text.
200
+ * Every free-text field is sanitised before embedding, so the authoritative
201
+ * `**Fingerprint:** \`<fp>\`` sentinel cannot be spoofed: `recommendation`
202
+ * (#388) and `description` go through sanitizeRecommendation() — description
203
+ * is line 1, BEFORE the real sentinel, and carries verbatim page HTML (axe
204
+ * results), while the extractor takes the FIRST match. `locator` goes through
205
+ * codeSpanContent() so it also cannot break out of its code span.
206
+ *
207
+ * Exported (#1331) so direct `createFinding` callers can build a body that
208
+ * passes its sentinel check instead of hand-writing the sentinel line.
188
209
  *
189
210
  * @param {object} finding
190
211
  * @param {string} fp - 16-char hex fingerprint
191
212
  * @returns {string}
192
213
  */
193
- function buildIssueBody(finding, fp) {
214
+ export function buildIssueBody(finding, fp) {
194
215
  const safeRecommendation = sanitizeRecommendation(finding.recommendation);
195
216
  const lines = [
196
- finding.description,
217
+ sanitizeRecommendation(finding.description),
197
218
  '',
198
219
  safeRecommendation ? `**Recommendation:** ${safeRecommendation}` : null,
199
220
  '',
200
221
  `**Fingerprint:** \`${fp}\``,
201
222
  `**Severity:** ${finding.severity}`,
202
223
  `**Check:** ${finding.checkId}`,
203
- `**Locator:** \`${finding.locator}\``,
224
+ `**Locator:** \`${codeSpanContent(finding.locator)}\``,
204
225
  ];
205
226
  return lines.filter((line) => line !== null).join('\n');
206
227
  }
@@ -528,9 +549,12 @@ export async function listExistingFindings({
528
549
  *
529
550
  * @param {object} opts
530
551
  * @param {string} [opts.project] - GitLab project path (--repo; #872: otherwise auto-detected)
531
- * @param {string} opts.fingerprint - 16-hex fingerprint (appended as sentinel)
552
+ * @param {string} opts.fingerprint - 16-hex fingerprint; NOT appended here — `body` must already carry it
532
553
  * @param {string} opts.title - issue title (no [Test] prefix added here — caller decides)
533
- * @param {string} opts.body - issue description body; must not exceed 65536 bytes (#389)
554
+ * @param {string} opts.body - issue description body; must not exceed 65536 bytes (#389) and must
555
+ * carry the `**Fingerprint:** \`<fingerprint>\`` sentinel for this same fingerprint (#1331) —
556
+ * build it with {@link buildIssueBody}
557
+
534
558
  * @param {string} [opts.labels='from:test-runner'] - comma-separated label string
535
559
  * @param {boolean} [opts.dryRun=false] - if true, return command without spawning
536
560
  * @param {number} [opts.maxBuffer=4194304] - maxBuffer for execFile (4 MB, #389)
@@ -590,6 +614,19 @@ export async function createFinding({
590
614
  const bodyLengthError = checkBodyLength(body);
591
615
  if (bodyLengthError) return bodyLengthError;
592
616
 
617
+ // #1331: dedup reads the fingerprint back out of the body on the next run, and
618
+ // nothing here appends it. A body without the matching sentinel would file an
619
+ // issue no later run can recognise — every run would then open a duplicate.
620
+ if (extractFingerprintFromBody(body) !== fingerprint) {
621
+ return {
622
+ ok: false,
623
+ error: {
624
+ code: 'VALIDATION',
625
+ message: 'body must carry a **Fingerprint:** sentinel matching fingerprint (use buildIssueBody)',
626
+ },
627
+ };
628
+ }
629
+
593
630
  const args = ['issue', 'create', '--title', title, '--label', labels, '--description', body];
594
631
 
595
632
  // #872: explicit project always wins; otherwise auto-detect a --repo spec
@@ -11,7 +11,8 @@
11
11
  * Pane 3 (bottom-right) — vcs-aware CI watch (poll-loop from detectVcsCommand)
12
12
  * Pane 4 (bottom-left) — tail -F .orchestrator/metrics/events.jsonl | jq select(wave|gate|spiral)
13
13
  * Pane 5 (optional) — agent-status telemetry (#565), only when withStatusPane is true:
14
- * poll-loop over .orchestrator/runtime/agent-status-current.json
14
+ * poll-loop over readCurrentStatus() from scripts/lib/agent-status.mjs,
15
+ * which reports provenance (live-map / rebuilt-log / stale-cache) — #1342.
15
16
  *
16
17
  * Debug layout pane map (user-facing numbering):
17
18
  * Pane 1 (top-left) — scratch shell (NO command — default tmux shell, AUQ-001 compliance)
@@ -58,6 +59,61 @@ function shellQuote(s) {
58
59
  return "'" + String(s).replace(/'/g, "'\\''") + "'";
59
60
  }
60
61
 
62
+ /** Poll interval (seconds) of the agent-status pane loop. */
63
+ const STATUS_PANE_POLL_SECONDS = 2;
64
+
65
+ /**
66
+ * Build the ONE-SHOT render command of the agent-status pane (#1342).
67
+ *
68
+ * The pane used to `jq` `.orchestrator/runtime/agent-status-current.json` directly.
69
+ * That file is a REBUILDABLE CACHE, not the source of truth: after a lock timeout
70
+ * (or a death between the ledger append and the map write) it shows `running` for
71
+ * an agent whose ledger already says `completed`. So the pane now goes through
72
+ * `readCurrentStatus()` — the only reader that folds the ledger tail and reports
73
+ * PROVENANCE — and prints that provenance in its header line:
74
+ *
75
+ * agent-status · source=<live-map|rebuilt-log|stale-cache|absent> · at=<ISO|n/a>[ · STALE][ · DEGRADED: <reasons>]
76
+ *
77
+ * A `stale-cache` source or any `degraded` reason is marked in TEXT (leading `⚠`
78
+ * plus the words STALE / DEGRADED), never by colour alone. `absent` (nothing on
79
+ * disk yet) stays UNMARKED — a fresh repo is not a degradation (HR-101).
80
+ *
81
+ * Exported so a consumer test can run the render ONCE instead of the poll loop.
82
+ *
83
+ * Shell-shape notes (both load-bearing):
84
+ * - the `-e` script is wrapped in SINGLE quotes, so the JS below uses only
85
+ * double-quoted strings: inside single quotes no `$`, backtick or interactive
86
+ * `!` history expansion can touch it.
87
+ * - the module path is resolved from THIS file's own URL (never a hardcoded home
88
+ * path), exactly as the sibling pane commands stay relative-to-cwd.
89
+ *
90
+ * @returns {string} a single shell command; prints the pane body once and exits.
91
+ */
92
+ export function buildStatusPaneRenderCommand() {
93
+ // `%27` guard: a single quote in the repo path would otherwise end the shell
94
+ // single-quoted `-e` argument.
95
+ const moduleHref = new URL('../agent-status.mjs', import.meta.url).href.replace(/'/g, '%27');
96
+ const js = [
97
+ `import {readCurrentStatus} from ${JSON.stringify(moduleHref)};`,
98
+ 'const v=readCurrentStatus({repoRoot:process.cwd()});',
99
+ 'const ids=Object.keys(v.entries).sort();',
100
+ // An EMPTY channel (no ledger yet, no cache yet) is the normal fresh-repo
101
+ // state, not a degradation — marking it would fire the warning on every
102
+ // session and teach the operator to ignore it (host-resources.md HR-101).
103
+ // `readCurrentStatus()` names that state `absent` and emits no `degraded`,
104
+ // so the unmarked case keys on the SOURCE, not on a reason allowlist.
105
+ 'const bare=v.source==="absent";',
106
+ 'const marks=[];',
107
+ 'if(!bare&&v.source==="stale-cache")marks.push("STALE");',
108
+ 'if(!bare&&v.degraded)marks.push("DEGRADED: "+v.degraded.reasons.join(","));',
109
+ 'const head=(marks.length>0?"\u26a0 ":"")+"agent-status \u00b7 source="+v.source+" \u00b7 at="+(v.at||"n/a")+(marks.length>0?" \u00b7 "+marks.join(" \u00b7 "):"");',
110
+ 'console.log(head);',
111
+ 'if(ids.length===0)console.log("no agent-status yet \u2014 set persistence:true + run a wave (see skills/wave-executor/wave-loop.md \u00a7 3a-bis)");',
112
+ 'for(const id of ids){const r=v.entries[id]||{};const what=typeof r.text==="string"?r.text:(r.step!==undefined?r.step+"/"+r.total+(r.label?" "+r.label:""):"unknown");console.log(id+" "+what+" "+(r.ts||"no-ts"));}',
113
+ ].join('');
114
+ return `node --input-type=module -e '${js}'`;
115
+ }
116
+
61
117
  // ---------------------------------------------------------------------------
62
118
  // renderDefaultLayout
63
119
  // ---------------------------------------------------------------------------
@@ -91,9 +147,11 @@ async function _renderDefaultLayoutInner({ sessionName, force, projectRoot, vcsC
91
147
  const pane2Cmd = `tail -F ${stateMdPath}`;
92
148
  const pane3Cmd = vcs.command;
93
149
  const pane4Cmd = `tail -F .orchestrator/metrics/events.jsonl | jq --unbuffered 'select(.event | test("wave|gate|spiral"))'`;
94
- // Pane 5 (optional, #565): poll the LWW agent-status map. The `|| echo` fallback
95
- // (mirrors Pane 3 style) keeps a missing file / absent jq from erroring the pane.
96
- const pane5Cmd = `while true; do clear; jq . .orchestrator/runtime/agent-status-current.json 2>/dev/null || echo 'no agent-status yet — set persistence:true + run a wave (see skills/wave-executor/wave-loop.md § 3a-bis)'; sleep 2; done`;
150
+ // Pane 5 (optional, #565 / #1342): poll `readCurrentStatus()` — NOT the cache
151
+ // file — so the pane can never present a stale `running` as live. The renderer
152
+ // prints its own "no agent-status yet" line when the channel is empty, and is
153
+ // exported (buildStatusPaneRenderCommand) so a test can run it once.
154
+ const pane5Cmd = `while true; do clear; ${buildStatusPaneRenderCommand()}; sleep ${STATUS_PANE_POLL_SECONDS}; done`;
97
155
 
98
156
  // 4. Session-collision check
99
157
  const collision = isSessionCollision(sessionName);