session-orchestrator 3.22.0 → 3.23.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/commands/autopilot-multi.md +14 -0
- package/.cursor/commands/autopilot.md +14 -0
- package/.cursor/commands/bootstrap.md +14 -0
- package/.cursor/commands/brainstorm.md +14 -0
- package/.cursor/commands/close.md +13 -0
- package/.cursor/commands/contract-version-bump.md +14 -0
- package/.cursor/commands/debug.md +14 -0
- package/.cursor/commands/discovery.md +14 -0
- package/.cursor/commands/dispatcher.md +14 -0
- package/.cursor/commands/eli5.md +14 -0
- package/.cursor/commands/eval.md +14 -0
- package/.cursor/commands/evolve.md +14 -0
- package/.cursor/commands/go.md +14 -0
- package/.cursor/commands/grill.md +14 -0
- package/.cursor/commands/harness-audit.md +13 -0
- package/.cursor/commands/journey-audit.md +14 -0
- package/.cursor/commands/memory-cleanup.md +14 -0
- package/.cursor/commands/persona-panel.md +14 -0
- package/.cursor/commands/plan.md +14 -0
- package/.cursor/commands/portfolio.md +14 -0
- package/.cursor/commands/reconcile.md +14 -0
- package/.cursor/commands/release.md +14 -0
- package/.cursor/commands/repo-audit.md +13 -0
- package/.cursor/commands/session.md +14 -0
- package/.cursor/commands/spinout.md +14 -0
- package/.cursor/commands/sunset-review.md +14 -0
- package/.cursor/commands/templates-ack.md +14 -0
- package/.cursor/commands/test.md +14 -0
- package/.cursor/hooks.json +60 -0
- package/.cursor/rules/000-session-orchestrator.mdc +8 -0
- package/.cursor/rules/010-session-workflow.mdc +9 -1
- package/.cursor/rules/020-quality-gates.mdc +1 -1
- package/.cursor/rules/030-wave-execution.mdc +1 -1
- package/.cursor/rules/050-plan.mdc +2 -2
- package/.cursor/rules/070-gitlab-ops.mdc +73 -57
- package/.cursor/rules/080-ecosystem-health.mdc +7 -7
- package/.cursor/skills/architecture/SKILL.md +13 -0
- package/.cursor/skills/autopilot/SKILL.md +12 -0
- package/.cursor/skills/bootstrap/SKILL.md +12 -0
- package/.cursor/skills/brainstorm/SKILL.md +13 -0
- package/.cursor/skills/claude-md-drift-check/SKILL.md +13 -0
- package/.cursor/skills/contract-version-bump/SKILL.md +12 -0
- package/.cursor/skills/convergence-monitoring/SKILL.md +12 -0
- package/.cursor/skills/daily/SKILL.md +12 -0
- package/.cursor/skills/debug/SKILL.md +13 -0
- package/.cursor/skills/discovery/SKILL.md +13 -0
- package/.cursor/skills/dispatcher/SKILL.md +13 -0
- package/.cursor/skills/docs-orchestrator/SKILL.md +13 -0
- package/.cursor/skills/domain-model/SKILL.md +13 -0
- package/.cursor/skills/ecosystem-health/SKILL.md +13 -0
- package/.cursor/skills/eli5/SKILL.md +13 -0
- package/.cursor/skills/eval/SKILL.md +12 -0
- package/.cursor/skills/evolve/SKILL.md +13 -0
- package/.cursor/skills/frontmatter-guard/SKILL.md +13 -0
- package/.cursor/skills/gitlab-ops/SKILL.md +13 -0
- package/.cursor/skills/gitlab-portfolio/SKILL.md +13 -0
- package/.cursor/skills/grill/SKILL.md +13 -0
- package/.cursor/skills/hook-development/SKILL.md +13 -0
- package/.cursor/skills/journey-audit/SKILL.md +13 -0
- package/.cursor/skills/mcp-builder/SKILL.md +13 -0
- package/.cursor/skills/memory-cleanup/SKILL.md +12 -0
- package/.cursor/skills/mode-selector/SKILL.md +13 -0
- package/.cursor/skills/npm-publish/SKILL.md +12 -0
- package/.cursor/skills/peekaboo-driver/SKILL.md +13 -0
- package/.cursor/skills/persona-panel/SKILL.md +12 -0
- package/.cursor/skills/plan/SKILL.md +13 -0
- package/.cursor/skills/playwright-driver/SKILL.md +13 -0
- package/.cursor/skills/quality-gates/SKILL.md +13 -0
- package/.cursor/skills/reconcile/SKILL.md +12 -0
- package/.cursor/skills/repo-audit/SKILL.md +13 -0
- package/.cursor/skills/session-end/SKILL.md +13 -0
- package/.cursor/skills/session-plan/SKILL.md +13 -0
- package/.cursor/skills/session-start/SKILL.md +13 -0
- package/.cursor/skills/skill-creator/SKILL.md +13 -0
- package/.cursor/skills/spinout/SKILL.md +12 -0
- package/.cursor/skills/sunset-review/SKILL.md +13 -0
- package/.cursor/skills/test-runner/SKILL.md +13 -0
- package/.cursor/skills/tmux-layout/SKILL.md +13 -0
- package/.cursor/skills/ubiquitous-language/SKILL.md +13 -0
- package/.cursor/skills/using-orchestrator/SKILL.md +13 -0
- package/.cursor/skills/vault-mirror/SKILL.md +13 -0
- package/.cursor/skills/vault-sync/SKILL.md +13 -0
- package/.cursor/skills/wave-executor/SKILL.md +13 -0
- package/.cursor/skills/write-executable-plan/SKILL.md +13 -0
- package/.mcp.json +4 -1
- package/CHANGELOG.md +168 -0
- package/README.md +18 -15
- package/agents/AGENTS.md +23 -4
- package/agents/code-implementer.md +2 -1
- package/agents/db-specialist.md +2 -1
- package/agents/docs-writer.md +3 -1
- package/agents/eval-judge.md +1 -1
- package/agents/session-reviewer.md +7 -1
- package/agents/test-writer.md +2 -1
- package/agents/ui-developer.md +2 -1
- package/commands/bootstrap.md +2 -2
- package/commands/close.md +3 -1
- package/commands/go.md +1 -1
- package/commands/journey-audit.md +43 -0
- package/docs/USER-GUIDE.md +2 -2
- package/docs/ci-setup.md +14 -0
- package/docs/codex-setup.md +64 -0
- package/docs/components.md +6 -6
- package/docs/cursor-setup.md +26 -47
- package/docs/events-schema.md +76 -4
- package/docs/github-mirror-protection.md +197 -0
- package/docs/pi-setup.md +2 -0
- package/docs/rule-authoring.md +3 -1
- package/docs/scope-collision-guard.md +49 -2
- package/docs/session-config-reference.md +26 -4
- package/docs/session-config-template.md +4 -3
- package/docs/telemetry.md +22 -0
- package/hooks/_lib/lock-bootstrap.mjs +8 -4
- package/hooks/_lib/vcs-create-matcher.mjs +397 -38
- package/hooks/enforce-scope.mjs +64 -0
- package/hooks/hooks-codex.json +1 -1
- package/hooks/hooks-cursor.json +201 -20
- package/hooks/hooks-pi.json +1 -1
- package/hooks/hooks.json +2 -2
- package/hooks/on-session-end.mjs +211 -10
- package/hooks/on-session-start.mjs +214 -11
- package/hooks/on-stop.mjs +48 -9
- package/hooks/post-subagent-discovery-validator.mjs +34 -3
- package/hooks/post-tool-batch-wave-signal.mjs +11 -2
- package/hooks/pre-bash-issue-budget.mjs +117 -4
- package/hooks/pre-bash-sessions-ledger-guard.mjs +159 -0
- package/hooks/pre-bash-staging-fence.mjs +4 -0
- package/hooks/pre-task-scope-disjoint.mjs +368 -35
- package/hooks/skill-invocation-telemetry.mjs +21 -10
- package/monitors/monitors.json +6 -0
- package/package.json +1 -1
- package/pi/prompts/journey-audit.md +12 -0
- package/rules/_index.md +9 -1
- package/rules/always-on/ask-via-tool.md +62 -0
- package/rules/always-on/bash-harness-pitfalls.md +168 -0
- package/rules/always-on/build-value.md +47 -0
- package/rules/always-on/cross-session-messaging.md +59 -0
- package/rules/always-on/loop-and-monitor.md +221 -0
- package/rules/always-on/parallel-sessions.md +142 -12
- package/rules/always-on/receiving-review.md +108 -0
- package/rules/always-on/test-value.md +40 -0
- package/rules/always-on/verification-before-completion.md +77 -0
- package/scripts/archive-closed-prds.mjs +258 -18
- package/scripts/autopilot.mjs +5 -0
- package/scripts/backfill-evidence-digest.mjs +376 -0
- package/scripts/cursor-install.mjs +89 -48
- package/scripts/export-hw-learnings.mjs +143 -2
- package/scripts/express-path.mjs +299 -0
- package/scripts/generate-cursor-adapter.mjs +253 -0
- package/scripts/github-protection-audit.mjs +358 -0
- package/scripts/lib/autopilot/worktree-pipeline.mjs +240 -16
- package/scripts/lib/build-live-signals.mjs +24 -5
- package/scripts/lib/ci-status-banner.mjs +158 -11
- package/scripts/lib/command-blocker.mjs +70 -0
- package/scripts/lib/config/reconcile.mjs +79 -4
- package/scripts/lib/config/section-extractor.mjs +235 -36
- package/scripts/lib/config-schema.mjs +9 -1
- package/scripts/lib/config.mjs +57 -6
- package/scripts/lib/convergence-monitor.mjs +13 -2
- package/scripts/lib/cursor-hook-bridge.mjs +443 -0
- package/scripts/lib/dispatcher/cli.mjs +2 -2
- package/scripts/lib/express-path.mjs +327 -0
- package/scripts/lib/file-lock.mjs +22 -4
- package/scripts/lib/gates/gate-full.mjs +81 -8
- package/scripts/lib/gates/gate-helpers.mjs +76 -15
- package/scripts/lib/git-config-drift.mjs +134 -5
- package/scripts/lib/host-identity.mjs +247 -2
- package/scripts/lib/instruction-budget-guard.mjs +31 -1
- package/scripts/lib/issue-budget.mjs +229 -30
- package/scripts/lib/learnings/io.mjs +55 -10
- package/scripts/lib/learnings/schema.mjs +95 -28
- package/scripts/lib/lock-reaper.mjs +7 -1
- package/scripts/lib/locks/staging-fence-lock.mjs +5 -1
- package/scripts/lib/locks/state-md-lock.mjs +8 -1
- package/scripts/lib/memory-banner.mjs +5 -2
- package/scripts/lib/memory-paths.mjs +15 -6
- package/scripts/lib/mode-selector/scoring.mjs +53 -6
- package/scripts/lib/platform.mjs +72 -9
- package/scripts/lib/plugin-root.mjs +143 -19
- package/scripts/lib/project-hygiene.mjs +43 -3
- package/scripts/lib/quality-gate.mjs +271 -13
- package/scripts/lib/reconcile/emitter.mjs +87 -19
- package/scripts/lib/reconcile/engine.mjs +281 -13
- package/scripts/lib/reconcile/idempotency.mjs +102 -1
- package/scripts/lib/reconcile/renderer.mjs +148 -3
- package/scripts/lib/reconcile/sanitize.mjs +40 -17
- package/scripts/lib/reconcile/writer.mjs +415 -84
- package/scripts/lib/rule-loader.mjs +37 -2
- package/scripts/lib/rules-sync.mjs +51 -8
- package/scripts/lib/scope-gate.mjs +90 -0
- package/scripts/lib/session-close-backfill.mjs +369 -28
- package/scripts/lib/session-discovery.mjs +13 -3
- package/scripts/lib/session-end/phase-skip.mjs +37 -4
- package/scripts/lib/session-end/worktree-cleanup.mjs +154 -7
- package/scripts/lib/session-id.mjs +30 -14
- package/scripts/lib/session-identity/own-session.mjs +159 -0
- package/scripts/lib/session-lock.mjs +85 -30
- package/scripts/lib/session-schema/normalizer.mjs +70 -3
- package/scripts/lib/session-schema/validator.mjs +40 -0
- package/scripts/lib/session-start-probes.mjs +608 -0
- package/scripts/lib/session-transition.mjs +277 -0
- package/scripts/lib/sessions-staleness-banner.mjs +124 -57
- package/scripts/lib/spiral-carryover.mjs +90 -9
- package/scripts/lib/state-md/frontmatter-mutators.mjs +41 -8
- package/scripts/lib/state-md/mission-status.mjs +350 -52
- package/scripts/lib/state-md/yaml-parser.mjs +145 -16
- package/scripts/lib/state-md.mjs +12 -2
- package/scripts/lib/telemetry/sync.mjs +46 -8
- package/scripts/lib/validate/check-agents.mjs +66 -0
- package/scripts/lib/validate/check-cursor-adapter.mjs +102 -0
- package/scripts/lib/validate/check-dead-bridge.mjs +24 -2
- package/scripts/lib/validate/check-doc-cli-commands.mjs +16 -32
- package/scripts/lib/validate/check-hooks-symmetry.mjs +29 -63
- package/scripts/lib/validate/check-playwright-mcp-canary.mjs +13 -22
- package/scripts/lib/validate/check-plugin-monitors.mjs +10 -4
- package/scripts/lib/validate/check-test-value-bans.mjs +165 -17
- package/scripts/lib/validate/check-unwired-features.mjs +340 -32
- package/scripts/lib/validate/repo-files.mjs +275 -0
- package/scripts/lib/validate-vendored-rules.mjs +229 -7
- package/scripts/lib/vault-mirror/process.mjs +99 -43
- package/scripts/lib/vault-mirror/telemetry.mjs +210 -0
- package/scripts/lib/vault-staleness-banner.mjs +76 -6
- package/scripts/lib/vault-status/board-writer.mjs +211 -10
- package/scripts/lib/vault-status/narrative-mirror.mjs +188 -8
- package/scripts/lib/wave-executor/foreign-dispatch.mjs +832 -0
- package/scripts/lib/wave-transcript-tail.mjs +869 -0
- package/scripts/materialize-wave-scope.mjs +209 -12
- package/scripts/mcp-server.sh +11 -2
- package/scripts/parse-config.mjs +65 -0
- package/scripts/token-audit.sh +9 -2
- package/scripts/validate-plugin.mjs +3 -0
- package/scripts/validate-wave-scope.mjs +67 -0
- package/scripts/vault-mirror.mjs +203 -34
- package/skills/_shared/monitor-patterns.md +31 -5
- package/skills/_shared/parallel-aware-auq.md +1 -1
- package/skills/_shared/parallel-aware-preamble.md +4 -2
- package/skills/_shared/platform-tools.md +11 -5
- package/skills/_shared/state-ownership.md +29 -2
- package/skills/autopilot/SKILL.md +5 -1
- package/skills/bootstrap/SKILL.md +3 -3
- package/skills/bootstrap/_shared-template.md +18 -10
- package/skills/bootstrap/deep-template.md +10 -6
- package/skills/bootstrap/fast-template.md +15 -8
- package/skills/bootstrap/standard-template.md +10 -6
- package/skills/claude-md-drift-check/checker.mjs +39 -11
- package/skills/dispatcher/SKILL.md +1 -1
- package/skills/journey-audit/SKILL.md +269 -0
- package/skills/peekaboo-driver/SKILL.md +15 -3
- package/skills/persona-panel/SKILL.md +1 -1
- package/skills/reconcile/SKILL.md +41 -1
- package/skills/session-end/SKILL.md +17 -4
- package/skills/session-end/metrics-collection.md +7 -4
- package/skills/session-end/phase-3-6-tail.md +11 -3
- package/skills/session-end/phase-3-7a-recommendations.md +16 -2
- package/skills/session-plan/SKILL.md +6 -1
- package/skills/session-plan/wave-template.md +1 -0
- package/skills/session-start/SKILL.md +30 -16
- package/skills/session-start/phase-7-5-mode-selector.md +15 -3
- package/skills/session-start/phase-8-5-express-path.md +77 -12
- package/skills/vault-sync/validator.mjs +31 -0
- package/skills/wave-executor/SKILL.md +4 -2
- package/skills/wave-executor/circuit-breaker.md +34 -9
- package/skills/wave-executor/wave-loop.md +102 -19
- package/templates/_shared/journey-manifest.md +110 -0
- package/templates/_shared/rules/parallel-sessions.md +0 -77
|
@@ -6,8 +6,15 @@
|
|
|
6
6
|
* - Flow-style integer arrays (`[1, 2, 3]`)
|
|
7
7
|
* - Block-style sequences of mappings (issue #244), e.g. `docs-tasks:` with
|
|
8
8
|
* indented `- key: value` entries. Only one nesting level supported.
|
|
9
|
+
* - Single-line FLOW MAPPINGS as list items (`- { k: v, … }`) — hand-written
|
|
10
|
+
* but valid YAML, parsed into the same plain object a block item yields and
|
|
11
|
+
* re-emitted in block notation on the next serialize (#1111).
|
|
9
12
|
*
|
|
10
13
|
* That is the full grammar permitted by skills/_shared/state-ownership.md.
|
|
14
|
+
* A list item this grammar cannot represent (a flow SEQUENCE `- [a, b]`, or a
|
|
15
|
+
* flow mapping whose interior is malformed) is dropped from ITS OWN LIST and
|
|
16
|
+
* reported on `parseStateMd(...).warnings` — never escalated to a null
|
|
17
|
+
* document. See `parseBlockValue` for why that scoping is load-bearing.
|
|
11
18
|
*
|
|
12
19
|
* Never throws. Returns null for unparseable input rather than raising.
|
|
13
20
|
*
|
|
@@ -35,17 +42,28 @@ const FRONTMATTER_RE = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/;
|
|
|
35
42
|
/**
|
|
36
43
|
* Parses a STATE.md file into frontmatter + body.
|
|
37
44
|
*
|
|
45
|
+
* `warnings` is OMITTED (not `null`, not `[]`) when the parse was clean, so a
|
|
46
|
+
* caller pinning the whole return with `toEqual({ frontmatter, body })` still
|
|
47
|
+
* matches and no consumer has to learn about it to keep working. When present
|
|
48
|
+
* it is a non-empty array of `{ key, index, reason }` records naming list items
|
|
49
|
+
* that were dropped from `frontmatter[key]` — `index` addresses the RAW source
|
|
50
|
+
* item position (dropped items included), so it stays quotable against the file
|
|
51
|
+
* even though the surviving array is shorter.
|
|
52
|
+
*
|
|
38
53
|
* @param {string} contents
|
|
39
|
-
* @returns {{frontmatter: object, body: string}|null}
|
|
54
|
+
* @returns {{frontmatter: object, body: string, warnings?: Array<{key: string, index: number, reason: string}>}|null}
|
|
40
55
|
*/
|
|
41
56
|
export function parseStateMd(contents) {
|
|
42
57
|
if (typeof contents !== 'string') return null;
|
|
43
58
|
const match = FRONTMATTER_RE.exec(contents);
|
|
44
59
|
if (!match) return null;
|
|
45
60
|
const [, fmText, body] = match;
|
|
46
|
-
const
|
|
61
|
+
const warnings = [];
|
|
62
|
+
const frontmatter = parseFrontmatter(fmText, warnings);
|
|
47
63
|
if (frontmatter === null) return null;
|
|
48
|
-
|
|
64
|
+
const parsed = { frontmatter, body: body.startsWith('\n') ? body.slice(1) : body };
|
|
65
|
+
if (warnings.length > 0) parsed.warnings = warnings;
|
|
66
|
+
return parsed;
|
|
49
67
|
}
|
|
50
68
|
|
|
51
69
|
/**
|
|
@@ -70,7 +88,7 @@ export function serializeStateMd({ frontmatter, body }) {
|
|
|
70
88
|
return `---\n${fmLines.join('\n')}\n---\n${bodyOut}`;
|
|
71
89
|
}
|
|
72
90
|
|
|
73
|
-
function parseFrontmatter(text) {
|
|
91
|
+
function parseFrontmatter(text, warnings) {
|
|
74
92
|
const out = {};
|
|
75
93
|
const lines = text.split(/\r?\n/);
|
|
76
94
|
let i = 0;
|
|
@@ -87,7 +105,7 @@ function parseFrontmatter(text) {
|
|
|
87
105
|
if (key === '') return null;
|
|
88
106
|
const valuePart = rstripped.slice(idx + 1).trim();
|
|
89
107
|
if (valuePart === '') {
|
|
90
|
-
const result = parseBlockValue(lines, i + 1);
|
|
108
|
+
const result = parseBlockValue(lines, i + 1, key, warnings);
|
|
91
109
|
if (result === null) return null;
|
|
92
110
|
out[key] = result.value;
|
|
93
111
|
i = result.nextIndex;
|
|
@@ -106,8 +124,27 @@ function parseFrontmatter(text) {
|
|
|
106
124
|
* no body) and `nextIndex === start` so the caller resumes at `start`.
|
|
107
125
|
* - `value === [...]` means a block sequence was consumed.
|
|
108
126
|
* Returns `null` on malformed block syntax.
|
|
127
|
+
*
|
|
128
|
+
* A list item that opens a YAML FLOW collection is resolved PER ITEM, never per
|
|
129
|
+
* document:
|
|
130
|
+
* - `- { id: m-1, task: "x", wave: 1, status: b }` parses into exactly the
|
|
131
|
+
* object the equivalent block item yields (and serializes back as block
|
|
132
|
+
* notation). Splitting it at the first colon — the #1111 bug, measured
|
|
133
|
+
* 2026-08-24 @ f0766e1 as `keys: ["{ id"] · status: undefined` — is what
|
|
134
|
+
* this must never do again.
|
|
135
|
+
* - Anything else flow-shaped (`- [a, b]`, or a `{ … }` whose interior is
|
|
136
|
+
* malformed) is dropped from THIS list with a `warnings` record. It cannot
|
|
137
|
+
* be kept: a non-mapping item makes `isBlockSeqOfMappings` false for the
|
|
138
|
+
* whole array, and the serializer would then emit the entire list through
|
|
139
|
+
* `serializeScalar` as `[object Object]`.
|
|
140
|
+
*
|
|
141
|
+
* Escalating either case to `null` for the WHOLE document — the first #1111 fix
|
|
142
|
+
* — is the failure this scoping exists to prevent: every mutator in this family
|
|
143
|
+
* opens with `parseStateMd(contents); if (parsed === null) return contents;`, so
|
|
144
|
+
* one hand-written item silently turned every STATE.md write into a no-op that
|
|
145
|
+
* reported success. Silent-wrong must not be traded for silent-absent.
|
|
109
146
|
*/
|
|
110
|
-
function parseBlockValue(lines, start) {
|
|
147
|
+
function parseBlockValue(lines, start, key, warnings) {
|
|
111
148
|
let i = start;
|
|
112
149
|
while (i < lines.length) {
|
|
113
150
|
const rstripped = lines[i].replace(/\s+$/, '');
|
|
@@ -124,6 +161,7 @@ function parseBlockValue(lines, start) {
|
|
|
124
161
|
const indent = bulletMatch[1];
|
|
125
162
|
const contIndent = indent + ' ';
|
|
126
163
|
const entries = [];
|
|
164
|
+
let itemIndex = 0;
|
|
127
165
|
while (i < lines.length) {
|
|
128
166
|
const rstripped = lines[i].replace(/\s+$/, '');
|
|
129
167
|
if (rstripped === '' || /^\s*#/.test(rstripped)) {
|
|
@@ -132,12 +170,26 @@ function parseBlockValue(lines, start) {
|
|
|
132
170
|
}
|
|
133
171
|
if (!rstripped.startsWith(indent + '- ')) break;
|
|
134
172
|
const firstBody = rstripped.slice(indent.length + 2);
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
173
|
+
// `entry === null` marks an item this grammar cannot represent: its
|
|
174
|
+
// continuation lines are still consumed below (so the walk stays aligned),
|
|
175
|
+
// but nothing is pushed onto `entries`.
|
|
176
|
+
let entry;
|
|
177
|
+
if (firstBody.startsWith('{')) {
|
|
178
|
+
entry = parseFlowMapping(firstBody);
|
|
179
|
+
if (entry === null) {
|
|
180
|
+
warnings.push({ key, index: itemIndex, reason: 'malformed-flow-mapping' });
|
|
181
|
+
}
|
|
182
|
+
} else if (firstBody.startsWith('[')) {
|
|
183
|
+
entry = null;
|
|
184
|
+
warnings.push({ key, index: itemIndex, reason: 'flow-sequence-item' });
|
|
185
|
+
} else {
|
|
186
|
+
const firstColon = firstBody.indexOf(':');
|
|
187
|
+
if (firstColon === -1) return null;
|
|
188
|
+
const firstKey = firstBody.slice(0, firstColon).trim();
|
|
189
|
+
if (firstKey === '') return null;
|
|
190
|
+
entry = {};
|
|
191
|
+
entry[firstKey] = parseScalar(firstBody.slice(firstColon + 1).trim());
|
|
192
|
+
}
|
|
141
193
|
i++;
|
|
142
194
|
while (i < lines.length) {
|
|
143
195
|
const inner = lines[i].replace(/\s+$/, '');
|
|
@@ -150,16 +202,93 @@ function parseBlockValue(lines, start) {
|
|
|
150
202
|
if (/^\s/.test(body)) return null;
|
|
151
203
|
const colon = body.indexOf(':');
|
|
152
204
|
if (colon === -1) return null;
|
|
153
|
-
const
|
|
154
|
-
if (
|
|
155
|
-
entry[
|
|
205
|
+
const contKey = body.slice(0, colon).trim();
|
|
206
|
+
if (contKey === '') return null;
|
|
207
|
+
if (entry !== null) entry[contKey] = parseScalar(body.slice(colon + 1).trim());
|
|
156
208
|
i++;
|
|
157
209
|
}
|
|
158
|
-
entries.push(entry);
|
|
210
|
+
if (entry !== null) entries.push(entry);
|
|
211
|
+
itemIndex++;
|
|
159
212
|
}
|
|
160
213
|
return { value: entries, nextIndex: i };
|
|
161
214
|
}
|
|
162
215
|
|
|
216
|
+
/**
|
|
217
|
+
* Splits a flow-collection interior on its TOP-LEVEL commas — the ones outside
|
|
218
|
+
* quotes and outside any nested `{}`/`[]`. A naive `split(',')` would cut
|
|
219
|
+
* `task: "a, b"` in half, which is the same class of first-separator mistake
|
|
220
|
+
* that produced the `{ id` key in the first place.
|
|
221
|
+
*
|
|
222
|
+
* @param {string} inner
|
|
223
|
+
* @returns {string[]}
|
|
224
|
+
*/
|
|
225
|
+
function splitFlowSegments(inner) {
|
|
226
|
+
const segments = [];
|
|
227
|
+
let depth = 0;
|
|
228
|
+
let quote = null;
|
|
229
|
+
let current = '';
|
|
230
|
+
for (let i = 0; i < inner.length; i++) {
|
|
231
|
+
const ch = inner[i];
|
|
232
|
+
if (quote !== null) {
|
|
233
|
+
current += ch;
|
|
234
|
+
if (ch === '\\' && quote === '"') {
|
|
235
|
+
current += inner[++i] ?? '';
|
|
236
|
+
} else if (ch === quote) {
|
|
237
|
+
quote = null;
|
|
238
|
+
}
|
|
239
|
+
continue;
|
|
240
|
+
}
|
|
241
|
+
if (ch === '"' || ch === "'") quote = ch;
|
|
242
|
+
else if (ch === '{' || ch === '[') depth++;
|
|
243
|
+
else if (ch === '}' || ch === ']') depth--;
|
|
244
|
+
else if (ch === ',' && depth === 0) {
|
|
245
|
+
segments.push(current);
|
|
246
|
+
current = '';
|
|
247
|
+
continue;
|
|
248
|
+
}
|
|
249
|
+
current += ch;
|
|
250
|
+
}
|
|
251
|
+
segments.push(current);
|
|
252
|
+
return segments;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Parses a SINGLE-LINE YAML flow mapping (`{ k: v, k2: "v, 2" }`) into a plain
|
|
257
|
+
* object, reusing `parseScalar` for each value so a flow item and the equivalent
|
|
258
|
+
* block item yield byte-identical results.
|
|
259
|
+
*
|
|
260
|
+
* Returns `null` when `raw` is not a well-formed single-line flow mapping —
|
|
261
|
+
* unterminated, an empty segment, or a segment with no `:`. Callers report that
|
|
262
|
+
* as a dropped item; nothing here throws.
|
|
263
|
+
*
|
|
264
|
+
* KNOWN CEILING: a NESTED flow mapping value (`{ id: m-1, meta: { a: 1 } }`)
|
|
265
|
+
* keeps `{ a: 1 }` as a STRING rather than an object — this subset has no
|
|
266
|
+
* nested-mapping representation and the serializer has no way to emit one. Flat
|
|
267
|
+
* flow mappings are the whole observed population (hand-written `mission-status`
|
|
268
|
+
* / `docs-tasks` items). Revisit if a nested flow value ever appears in a real
|
|
269
|
+
* STATE.md.
|
|
270
|
+
*
|
|
271
|
+
* @param {string} raw
|
|
272
|
+
* @returns {object|null}
|
|
273
|
+
*/
|
|
274
|
+
function parseFlowMapping(raw) {
|
|
275
|
+
const trimmed = raw.trim();
|
|
276
|
+
if (!trimmed.startsWith('{') || !trimmed.endsWith('}')) return null;
|
|
277
|
+
const inner = trimmed.slice(1, -1).trim();
|
|
278
|
+
const out = {};
|
|
279
|
+
if (inner === '') return out;
|
|
280
|
+
for (const segment of splitFlowSegments(inner)) {
|
|
281
|
+
const part = segment.trim();
|
|
282
|
+
if (part === '') return null;
|
|
283
|
+
const colon = part.indexOf(':');
|
|
284
|
+
if (colon === -1) return null;
|
|
285
|
+
const key = part.slice(0, colon).trim();
|
|
286
|
+
if (key === '') return null;
|
|
287
|
+
out[key] = parseScalar(part.slice(colon + 1).trim());
|
|
288
|
+
}
|
|
289
|
+
return out;
|
|
290
|
+
}
|
|
291
|
+
|
|
163
292
|
function parseScalar(raw) {
|
|
164
293
|
if (raw === '' || raw === 'null' || raw === '~') return null;
|
|
165
294
|
if (raw === 'true') return true;
|
package/scripts/lib/state-md.mjs
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* STATE.md helpers (issue #184).
|
|
3
3
|
*
|
|
4
|
-
* Thin barrel — re-exports
|
|
4
|
+
* Thin barrel — re-exports every public symbol of the split submodules.
|
|
5
5
|
* Implementation lives in scripts/lib/state-md/*.mjs.
|
|
6
6
|
*
|
|
7
7
|
* @see scripts/lib/state-md/yaml-parser.mjs parseStateMd, serializeStateMd
|
|
8
8
|
* @see scripts/lib/state-md/frontmatter-mutators.mjs touchUpdatedField, updateFrontmatterFields
|
|
9
9
|
* @see scripts/lib/state-md/body-sections.mjs readCurrentTask, appendDeviation, markExpressPathComplete, appendWhatNotToRetry, readWhatNotToRetry, readOpenQuestions, appendOpenQuestion, markOpenQuestionAnswered
|
|
10
|
-
* @see scripts/lib/state-md/mission-status.mjs parseMissionStatus, writeMissionStatus, setMissionStatus, readMissionStatus
|
|
10
|
+
* @see scripts/lib/state-md/mission-status.mjs parseMissionStatus, parseMissionStatusStrict, MISSION_STATUS_VALUES, writeMissionStatus, setMissionStatus, setMissionStatusDetailed, readMissionStatus, recoverFrontmatterMissionStatusDetailed, writeMissionStatusOnDisk, setMissionStatusOnDisk
|
|
11
11
|
* @see scripts/lib/state-md/recommendations.mjs parseRecommendations
|
|
12
12
|
*/
|
|
13
13
|
|
|
@@ -43,11 +43,21 @@ export {
|
|
|
43
43
|
|
|
44
44
|
export {
|
|
45
45
|
parseMissionStatus,
|
|
46
|
+
// The strict reader and the status vocabulary are part of the same public
|
|
47
|
+
// surface as `parseMissionStatus` (#1111) — a consumer that imports from this
|
|
48
|
+
// barrel (the documented entry point) reached neither until this re-export
|
|
49
|
+
// landed: a static `import { parseMissionStatusStrict } from '.../state-md.mjs'`
|
|
50
|
+
// failed at link time, and the `await import()` form — the one hooks and
|
|
51
|
+
// lazy loaders use — yielded `undefined` and failed only at the call.
|
|
52
|
+
parseMissionStatusStrict,
|
|
53
|
+
MISSION_STATUS_VALUES,
|
|
46
54
|
writeMissionStatus,
|
|
47
55
|
setMissionStatus,
|
|
48
56
|
readMissionStatus,
|
|
49
57
|
writeMissionStatusOnDisk,
|
|
50
58
|
setMissionStatusOnDisk,
|
|
59
|
+
recoverFrontmatterMissionStatusDetailed,
|
|
60
|
+
setMissionStatusDetailed,
|
|
51
61
|
} from './state-md/mission-status.mjs';
|
|
52
62
|
|
|
53
63
|
export { parseRecommendations } from './state-md/recommendations.mjs';
|
|
@@ -287,25 +287,63 @@ export async function flush({
|
|
|
287
287
|
// ---------------------------------------------------------------------------
|
|
288
288
|
|
|
289
289
|
/**
|
|
290
|
-
* Whether a daily-fallback flush is due
|
|
291
|
-
* have passed since the last successful flush
|
|
292
|
-
* counts as
|
|
290
|
+
* Whether a daily-fallback flush is due. Two independent ways to be due, both
|
|
291
|
+
* requiring that more than 24h have passed since the last successful flush
|
|
292
|
+
* (never-flushed counts as infinitely stale):
|
|
293
|
+
*
|
|
294
|
+
* (a) RETRY — the offline queue is non-empty: earlier sends failed and the
|
|
295
|
+
* backlog deserves another attempt.
|
|
296
|
+
* (b) CATCH-UP — the queue is empty, but `<metricsDir>/sessions.jsonl` ends on
|
|
297
|
+
* a record whose `completed_at` is NEWER than `last_flush_at`: a session
|
|
298
|
+
* closed since the last successful send and produced no ping.
|
|
299
|
+
*
|
|
300
|
+
* (b) is the reason this predicate exists at all. While it was queue-only, the
|
|
301
|
+
* "daily fallback" could ONLY re-send what had already failed to send — it could
|
|
302
|
+
* never originate a ping. Measured 2026-08-23 (#1138): 588 session closes across
|
|
303
|
+
* 13 repos produced 82 ingest records (~14%), because the ONE writer of the
|
|
304
|
+
* queue is a failed `flush()`, and a `flush()` that never runs never fails.
|
|
305
|
+
*
|
|
306
|
+
* Cheap in the common case: the staleness gate is checked FIRST (one small
|
|
307
|
+
* telemetry.json read), so a host that flushed within the last 24h returns
|
|
308
|
+
* before touching either the queue or the session ledger.
|
|
309
|
+
*
|
|
310
|
+
* Never throws.
|
|
293
311
|
*
|
|
294
312
|
* @param {object} [opts]
|
|
295
313
|
* @param {string} [opts.statePath] telemetry.json path override.
|
|
296
314
|
* @param {string} [opts.queuePath] queue path override.
|
|
315
|
+
* @param {string} [opts.metricsDir] Metrics dir for the (b) catch-up probe
|
|
316
|
+
* (default `<cwd>/.orchestrator/metrics`).
|
|
297
317
|
* @param {number} [opts.now] Reference time in epoch-ms (default Date.now()).
|
|
298
318
|
* @returns {boolean}
|
|
299
319
|
*/
|
|
300
|
-
export function shouldDailyFlush({ statePath, queuePath, now = Date.now() } = {}) {
|
|
320
|
+
export function shouldDailyFlush({ statePath, queuePath, metricsDir, now = Date.now() } = {}) {
|
|
301
321
|
try {
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
322
|
+
// Staleness gate — shared by BOTH disjuncts, so it runs first and short-
|
|
323
|
+
// circuits the two file probes below on every fresh host.
|
|
305
324
|
const { record } = readTelemetryState({ path: statePath });
|
|
306
325
|
const raw = record?.last_flush_at;
|
|
307
326
|
const lastMs = typeof raw === 'string' && !Number.isNaN(Date.parse(raw)) ? Date.parse(raw) : 0;
|
|
308
|
-
|
|
327
|
+
if (now - lastMs <= DAILY_FLUSH_MS) return false;
|
|
328
|
+
|
|
329
|
+
// (a) Retry an existing backlog.
|
|
330
|
+
const { count } = queueStats({ path: queuePath });
|
|
331
|
+
if (count > 0) return true;
|
|
332
|
+
|
|
333
|
+
// (b) Catch-up: a session completed after the last successful flush.
|
|
334
|
+
// Reuses buildBatch's reader — same file, same skipInvalid tolerance,
|
|
335
|
+
// no second parser.
|
|
336
|
+
//
|
|
337
|
+
// Deliberate simplification (named ceiling): this parses the WHOLE
|
|
338
|
+
// sessions.jsonl to look at its last record. At the observed ledger size
|
|
339
|
+
// (271 records / 292 KB in this repo, 2026-08-23) that is sub-millisecond
|
|
340
|
+
// and it only runs once the 24h gate above has already passed. Revisit
|
|
341
|
+
// with a tail-read if any repo's sessions.jsonl passes ~10 MB.
|
|
342
|
+
const dir = metricsDir || path.join(process.cwd(), '.orchestrator', 'metrics');
|
|
343
|
+
const sessions = readJsonlFile(path.join(dir, 'sessions.jsonl'), { skipInvalid: true });
|
|
344
|
+
const last = sessions.length > 0 ? sessions[sessions.length - 1] : null;
|
|
345
|
+
const completedMs = Date.parse(last?.completed_at);
|
|
346
|
+
return !Number.isNaN(completedMs) && completedMs > lastMs;
|
|
309
347
|
} catch {
|
|
310
348
|
return false;
|
|
311
349
|
}
|
|
@@ -10,6 +10,7 @@ import { fileURLToPath } from 'node:url';
|
|
|
10
10
|
import yaml from 'js-yaml';
|
|
11
11
|
import { ALLOWED_MODEL_ALIASES, MODEL_ID_RE } from '../agent-frontmatter.mjs';
|
|
12
12
|
import { extractInitialFrontmatter } from './frontmatter-block.mjs';
|
|
13
|
+
import { parseFrontmatterDescription } from '../description-surface.mjs';
|
|
13
14
|
|
|
14
15
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
15
16
|
|
|
@@ -136,6 +137,57 @@ function parseToolsValue(toolsVal) {
|
|
|
136
137
|
return v.split(',').map((t) => t.trim()).filter(Boolean);
|
|
137
138
|
}
|
|
138
139
|
|
|
140
|
+
/**
|
|
141
|
+
* Detect a `description:` whose YAML-PARSED value is a silently truncated
|
|
142
|
+
* PREFIX of what the file actually says (#1029).
|
|
143
|
+
*
|
|
144
|
+
* An unquoted (plain) YAML scalar ends at the first ` #`: everything from there
|
|
145
|
+
* on is a comment. The block still parses, so every other rule in this gate
|
|
146
|
+
* stays green while the agent picker receives a stub — `agents/eval-judge.md`
|
|
147
|
+
* shipped 51 of 1,180 characters (96% dropped, measured 2026-08-24) and nothing
|
|
148
|
+
* complained. This is the concrete case the Check-6 preamble describes in prose.
|
|
149
|
+
*
|
|
150
|
+
* Detected by COMPARISON, never by pattern: the parsed value must be a strict
|
|
151
|
+
* prefix of the raw scalar AND the dropped tail must open a comment. A
|
|
152
|
+
* legitimately quoted description whose text merely contains `#` parses in full
|
|
153
|
+
* (parsed === raw, no length gap) and is never flagged; a quoted description
|
|
154
|
+
* carrying YAML escapes diverges from its raw form and fails the prefix test.
|
|
155
|
+
*
|
|
156
|
+
* Raw extraction is delegated to `parseFrontmatterDescription` (the sibling
|
|
157
|
+
* measurement probe already owns it, block scalars and quote-stripping included)
|
|
158
|
+
* rather than re-derived from a fourth regex here.
|
|
159
|
+
*
|
|
160
|
+
* @param {string} content - the agent file's raw content
|
|
161
|
+
* @param {Record<string, unknown>} data - the parsed frontmatter mapping
|
|
162
|
+
* @param {string} frontmatter - the frontmatter block's inner text
|
|
163
|
+
* @returns {{ parsedLength: number, rawLength: number } | null} null when intact
|
|
164
|
+
*/
|
|
165
|
+
function detectDescriptionTruncation(content, data, frontmatter) {
|
|
166
|
+
if (!hasField(frontmatter, 'description')) return null;
|
|
167
|
+
|
|
168
|
+
// A `description:` key whose WHOLE value is a comment (`description: #803 …`)
|
|
169
|
+
// parses to null — a 0-character description, the same defect at 100% loss.
|
|
170
|
+
const parsedValue =
|
|
171
|
+
typeof data?.description === 'string'
|
|
172
|
+
? data.description
|
|
173
|
+
: data?.description === null || data?.description === undefined
|
|
174
|
+
? ''
|
|
175
|
+
: null;
|
|
176
|
+
if (parsedValue === null) return null;
|
|
177
|
+
|
|
178
|
+
const rawField = parseFrontmatterDescription(content);
|
|
179
|
+
// Block scalars carry no comment semantics and are rejected outright by the
|
|
180
|
+
// inline-string rule below — not this rule's business.
|
|
181
|
+
if (!rawField || rawField.isBlockScalar) return null;
|
|
182
|
+
|
|
183
|
+
const raw = rawField.raw;
|
|
184
|
+
if (raw.length <= parsedValue.length) return null;
|
|
185
|
+
if (!raw.startsWith(parsedValue)) return null;
|
|
186
|
+
if (!/(^|\s)#/.test(raw.slice(parsedValue.length))) return null;
|
|
187
|
+
|
|
188
|
+
return { parsedLength: parsedValue.length, rawLength: raw.length };
|
|
189
|
+
}
|
|
190
|
+
|
|
139
191
|
// ============================================================================
|
|
140
192
|
// Check 6: Agent .md files have valid YAML frontmatter
|
|
141
193
|
// ============================================================================
|
|
@@ -226,6 +278,20 @@ if (mdFiles.length === 0) {
|
|
|
226
278
|
}
|
|
227
279
|
parseable++;
|
|
228
280
|
|
|
281
|
+
// ------------------------------------------------------------------
|
|
282
|
+
// Prefix-truncation rule (#1029) — the ` #` case the preamble above names.
|
|
283
|
+
// Runs BEFORE the field rules: a description truncated to a stub still
|
|
284
|
+
// satisfies every one of them, so this is the only place the loss shows up.
|
|
285
|
+
// ------------------------------------------------------------------
|
|
286
|
+
const truncated = detectDescriptionTruncation(content, parsedFm.data, frontmatter);
|
|
287
|
+
if (truncated) {
|
|
288
|
+
fail(
|
|
289
|
+
`${agentName}: description is silently truncated by an unquoted ' #' — YAML parses ` +
|
|
290
|
+
`${truncated.parsedLength} of ${truncated.rawLength} chars and reads the rest as a comment; ` +
|
|
291
|
+
`quote the description (double-quoted, escaping any " inside) so the full text parses`,
|
|
292
|
+
);
|
|
293
|
+
}
|
|
294
|
+
|
|
229
295
|
// ------------------------------------------------------------------
|
|
230
296
|
// Required fields check
|
|
231
297
|
// ------------------------------------------------------------------
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// check-cursor-adapter.mjs — Ensure Cursor-native commands, skills, and hooks stay wired.
|
|
3
|
+
// Usage: check-cursor-adapter.mjs <plugin-root>
|
|
4
|
+
// Exit 0 = all checks passed, 1 = at least one failure.
|
|
5
|
+
|
|
6
|
+
import { spawnSync } from 'node:child_process';
|
|
7
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
8
|
+
import { join } from 'node:path';
|
|
9
|
+
|
|
10
|
+
import { CURSOR_TO_CANONICAL_EVENT } from '../cursor-hook-bridge.mjs';
|
|
11
|
+
|
|
12
|
+
const pluginRoot = process.argv[2];
|
|
13
|
+
if (!pluginRoot) {
|
|
14
|
+
console.error('Usage: check-cursor-adapter.mjs <plugin-root>');
|
|
15
|
+
process.exit(1);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
let passed = 0;
|
|
19
|
+
let failed = 0;
|
|
20
|
+
|
|
21
|
+
function pass(msg) { console.log(` PASS: ${msg}`); passed += 1; }
|
|
22
|
+
function fail(msg) { console.log(` FAIL: ${msg}`); failed += 1; }
|
|
23
|
+
|
|
24
|
+
console.log('--- Check 1: generated Cursor command and skill wrappers ---');
|
|
25
|
+
|
|
26
|
+
const generator = join(pluginRoot, 'scripts', 'generate-cursor-adapter.mjs');
|
|
27
|
+
if (!existsSync(generator)) {
|
|
28
|
+
fail('scripts/generate-cursor-adapter.mjs exists');
|
|
29
|
+
} else {
|
|
30
|
+
const result = spawnSync(process.execPath, [generator, '--check'], {
|
|
31
|
+
cwd: pluginRoot,
|
|
32
|
+
encoding: 'utf8',
|
|
33
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
34
|
+
});
|
|
35
|
+
if (result.status === 0) {
|
|
36
|
+
pass('.cursor/commands and .cursor/skills are up to date with commands/ and skills/');
|
|
37
|
+
} else {
|
|
38
|
+
const detail = ((result.stdout ?? '') + (result.stderr ?? '')).trim();
|
|
39
|
+
fail(`Cursor adapter wrappers are stale${detail ? `: ${detail}` : ''}`);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
console.log('');
|
|
44
|
+
console.log('--- Check 2: .cursor/hooks.json native manifest ---');
|
|
45
|
+
|
|
46
|
+
const hooksPath = join(pluginRoot, '.cursor', 'hooks.json');
|
|
47
|
+
if (!existsSync(hooksPath)) {
|
|
48
|
+
fail('.cursor/hooks.json exists');
|
|
49
|
+
} else {
|
|
50
|
+
let hooksJson;
|
|
51
|
+
try {
|
|
52
|
+
hooksJson = JSON.parse(readFileSync(hooksPath, 'utf8'));
|
|
53
|
+
pass('.cursor/hooks.json is valid JSON');
|
|
54
|
+
} catch (err) {
|
|
55
|
+
fail(`.cursor/hooks.json is not valid JSON: ${err.message}`);
|
|
56
|
+
hooksJson = null;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (hooksJson) {
|
|
60
|
+
if (hooksJson.version === 1) {
|
|
61
|
+
pass('.cursor/hooks.json version is 1');
|
|
62
|
+
} else {
|
|
63
|
+
fail('.cursor/hooks.json version must be 1');
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const declared = Object.keys(hooksJson.hooks || {});
|
|
67
|
+
const expected = Object.keys(CURSOR_TO_CANONICAL_EVENT);
|
|
68
|
+
const missing = expected.filter((event) => !declared.includes(event));
|
|
69
|
+
const extra = declared.filter((event) => !expected.includes(event));
|
|
70
|
+
if (missing.length === 0 && extra.length === 0) {
|
|
71
|
+
pass(`.cursor/hooks.json events match CURSOR_TO_CANONICAL_EVENT (${expected.length})`);
|
|
72
|
+
} else {
|
|
73
|
+
if (missing.length > 0) fail(`.cursor/hooks.json missing events: ${missing.join(', ')}`);
|
|
74
|
+
if (extra.length > 0) fail(`.cursor/hooks.json extra events: ${extra.join(', ')}`);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const bridgeRef = 'scripts/lib/cursor-hook-bridge.mjs';
|
|
78
|
+
const allPointAtBridge = declared.every((event) => {
|
|
79
|
+
const entries = Array.isArray(hooksJson.hooks[event]) ? hooksJson.hooks[event] : [];
|
|
80
|
+
return entries.some((entry) => typeof entry.command === 'string' && entry.command.includes(bridgeRef) && entry.command.includes(`--event ${event}`));
|
|
81
|
+
});
|
|
82
|
+
if (allPointAtBridge) {
|
|
83
|
+
pass('.cursor/hooks.json commands invoke cursor-hook-bridge.mjs with --event');
|
|
84
|
+
} else {
|
|
85
|
+
fail('.cursor/hooks.json commands must invoke scripts/lib/cursor-hook-bridge.mjs --event <name>');
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
console.log('');
|
|
91
|
+
console.log('--- Check 3: Cursor hook bridge module ---');
|
|
92
|
+
|
|
93
|
+
const bridgePath = join(pluginRoot, 'scripts', 'lib', 'cursor-hook-bridge.mjs');
|
|
94
|
+
if (existsSync(bridgePath)) {
|
|
95
|
+
pass('scripts/lib/cursor-hook-bridge.mjs exists');
|
|
96
|
+
} else {
|
|
97
|
+
fail('scripts/lib/cursor-hook-bridge.mjs exists');
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
console.log('');
|
|
101
|
+
console.log(`Results: ${passed} passed, ${failed} failed`);
|
|
102
|
+
process.exit(failed > 0 ? 1 : 0);
|
|
@@ -27,6 +27,7 @@ import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs';
|
|
|
27
27
|
import path from 'node:path';
|
|
28
28
|
import { DETECTORS } from './dead-bridge-detectors.mjs';
|
|
29
29
|
import * as corpus from './dead-bridge-corpus.mjs';
|
|
30
|
+
import { listRepoFiles } from './repo-files.mjs';
|
|
30
31
|
|
|
31
32
|
const DEFAULT_EXTS = ['.mjs', '.md'];
|
|
32
33
|
|
|
@@ -90,10 +91,31 @@ function walk(absDir, exts) {
|
|
|
90
91
|
* }}
|
|
91
92
|
*/
|
|
92
93
|
export function buildRepoContext(pluginRoot) {
|
|
94
|
+
/**
|
|
95
|
+
* Enumerate `absDir` from the git INDEX when it lies inside the plugin
|
|
96
|
+
* root, falling back to the raw walk otherwise (#1143). The walk cannot see
|
|
97
|
+
* `.gitignore`, so a worktree created under this repo's own
|
|
98
|
+
* `.claude/worktrees/` convention — or any vendored/generated tree, since
|
|
99
|
+
* this walk carries no exclusion set at all — enters the detector corpus as
|
|
100
|
+
* repository content. A detector then reads a PEER checkout's copy of a
|
|
101
|
+
* skill or rule file and counts it as an independent bridge.
|
|
102
|
+
*
|
|
103
|
+
* The containment guard matters: a corpus entry outside the root has no
|
|
104
|
+
* index to consult, and an `ls-files` pathspec of `../..` is an error, not
|
|
105
|
+
* an empty answer.
|
|
106
|
+
* @param {string} absDir
|
|
107
|
+
* @param {string[] | null} exts
|
|
108
|
+
* @returns {string[]}
|
|
109
|
+
*/
|
|
110
|
+
const listUnder = (absDir, exts) => {
|
|
111
|
+
const rel = path.relative(pluginRoot, absDir);
|
|
112
|
+
if (rel === '' || rel.startsWith('..') || path.isAbsolute(rel)) return walk(absDir, exts);
|
|
113
|
+
return listRepoFiles(pluginRoot, { dirs: [rel], exts });
|
|
114
|
+
};
|
|
93
115
|
return {
|
|
94
116
|
pluginRoot,
|
|
95
|
-
listMdFiles: (absDir) =>
|
|
96
|
-
listFiles: (absDir, exts = DEFAULT_EXTS) =>
|
|
117
|
+
listMdFiles: (absDir) => listUnder(absDir, ['.md']),
|
|
118
|
+
listFiles: (absDir, exts = DEFAULT_EXTS) => listUnder(absDir, exts),
|
|
97
119
|
readText: (absPath) => readFileSync(absPath, 'utf8'),
|
|
98
120
|
exists: (absPath) => existsSync(absPath),
|
|
99
121
|
};
|
|
@@ -62,10 +62,11 @@
|
|
|
62
62
|
* Verified against glab 1.91.0 / gh 2.86.0 (2026-08-15).
|
|
63
63
|
*/
|
|
64
64
|
|
|
65
|
-
import {
|
|
65
|
+
import { readFileSync } from 'node:fs';
|
|
66
66
|
import path from 'node:path';
|
|
67
67
|
import { spawnSync } from 'node:child_process';
|
|
68
68
|
import { pathToFileURL } from 'node:url';
|
|
69
|
+
import { listRepoFiles } from './repo-files.mjs';
|
|
69
70
|
|
|
70
71
|
/** Directories walked for documentation. Mirrors check-vcs-repo-flag.mjs. */
|
|
71
72
|
const SCAN_DIRS = Object.freeze([
|
|
@@ -80,8 +81,6 @@ const SCAN_DIRS = Object.freeze([
|
|
|
80
81
|
]);
|
|
81
82
|
|
|
82
83
|
/** Never descend into these. */
|
|
83
|
-
const EXCLUDED_DIRS = Object.freeze(['node_modules', '.git', 'dist', 'build', 'coverage']);
|
|
84
|
-
|
|
85
84
|
/** Fence languages whose body is shell. Anything else is prose. */
|
|
86
85
|
const SHELL_LANGS = Object.freeze(new Set(['bash', 'sh', 'shell', 'console', 'zsh']));
|
|
87
86
|
|
|
@@ -296,28 +295,6 @@ function probeHelp(bin, group, cache) {
|
|
|
296
295
|
return entry;
|
|
297
296
|
}
|
|
298
297
|
|
|
299
|
-
/**
|
|
300
|
-
* Recursively collect `.md` files.
|
|
301
|
-
*
|
|
302
|
-
* @param {string} directory
|
|
303
|
-
* @param {string[]} acc
|
|
304
|
-
* @returns {string[]}
|
|
305
|
-
*/
|
|
306
|
-
function walk(directory, acc = []) {
|
|
307
|
-
if (!existsSync(directory)) return acc;
|
|
308
|
-
for (const entry of readdirSync(directory, { withFileTypes: true })) {
|
|
309
|
-
if (entry.isSymbolicLink()) continue;
|
|
310
|
-
if (EXCLUDED_DIRS.includes(entry.name)) continue;
|
|
311
|
-
const fullPath = path.join(directory, entry.name);
|
|
312
|
-
if (entry.isDirectory()) {
|
|
313
|
-
walk(fullPath, acc);
|
|
314
|
-
continue;
|
|
315
|
-
}
|
|
316
|
-
if (entry.isFile() && path.extname(entry.name) === '.md') acc.push(fullPath);
|
|
317
|
-
}
|
|
318
|
-
return acc;
|
|
319
|
-
}
|
|
320
|
-
|
|
321
298
|
/** @param {string} text @returns {string} */
|
|
322
299
|
function clampSnippet(text) {
|
|
323
300
|
const flat = text.replace(/\s+/g, ' ').trim();
|
|
@@ -358,13 +335,20 @@ export function inspectDocCliCommands(pluginRoot) {
|
|
|
358
335
|
/** @type {string[]} */
|
|
359
336
|
let files;
|
|
360
337
|
try {
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
338
|
+
// The index, not the filesystem (#1143). A `readdirSync` walk cannot see
|
|
339
|
+
// `.gitignore`, so every ignored artefact under a scan root entered this
|
|
340
|
+
// census as if it were documentation: measured 2026-08-26 on a clean
|
|
341
|
+
// checkout with NO worktree present, 4 such files — `.claude/STATE.md`
|
|
342
|
+
// (per-session mutable state) and three gitignored `docs/specs/*.md`. A
|
|
343
|
+
// worktree under this repo's own `.claude/worktrees/` convention would
|
|
344
|
+
// add a complete second copy of every scanned doc on top of that.
|
|
345
|
+
// One enumeration covers both populations this check has always scanned:
|
|
346
|
+
// the SCAN_DIRS subtrees, and the repo-root `.md` files at depth 0.
|
|
347
|
+
const scanDirs = new Set(SCAN_DIRS);
|
|
348
|
+
files = listRepoFiles(pluginRoot, { exts: ['.md'] }).filter((absolute) => {
|
|
349
|
+
const segments = path.relative(pluginRoot, absolute).split(path.sep);
|
|
350
|
+
return segments.length === 1 || scanDirs.has(segments[0]);
|
|
351
|
+
});
|
|
368
352
|
} catch (error) {
|
|
369
353
|
findings.push({
|
|
370
354
|
kind: 'tool-error',
|