session-orchestrator 5.2.0 → 5.3.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/architecture/SKILL.md +3 -1
- package/.agents/skills/autopilot/SKILL.md +5 -1
- package/.agents/skills/autopilot/agents/openai.yaml +5 -0
- package/.agents/skills/bootstrap/SKILL.md +5 -1
- package/.agents/skills/bootstrap/agents/openai.yaml +5 -0
- package/.agents/skills/brainstorm/SKILL.md +5 -1
- package/.agents/skills/brainstorm/agents/openai.yaml +5 -0
- package/.agents/skills/claude-md-drift-check/SKILL.md +3 -1
- package/.agents/skills/close/SKILL.md +5 -1
- package/.agents/skills/close/agents/openai.yaml +5 -0
- package/.agents/skills/convergence-monitoring/SKILL.md +4 -2
- package/.agents/skills/debug/SKILL.md +5 -1
- package/.agents/skills/debug/agents/openai.yaml +5 -0
- package/.agents/skills/discovery/SKILL.md +5 -1
- package/.agents/skills/discovery/agents/openai.yaml +5 -0
- package/.agents/skills/dispatcher/SKILL.md +5 -1
- package/.agents/skills/dispatcher/agents/openai.yaml +5 -0
- package/.agents/skills/docs-orchestrator/SKILL.md +3 -1
- package/.agents/skills/ecosystem-health/SKILL.md +3 -1
- package/.agents/skills/eli5/SKILL.md +5 -1
- package/.agents/skills/eli5/agents/openai.yaml +5 -0
- package/.agents/skills/eval/SKILL.md +6 -2
- package/.agents/skills/eval/agents/openai.yaml +5 -0
- package/.agents/skills/evolve/SKILL.md +6 -2
- package/.agents/skills/evolve/agents/openai.yaml +5 -0
- package/.agents/skills/frontmatter-guard/SKILL.md +3 -1
- package/.agents/skills/gitlab-ops/SKILL.md +3 -1
- package/.agents/skills/gitlab-portfolio/SKILL.md +3 -1
- package/.agents/skills/go/SKILL.md +5 -1
- package/.agents/skills/go/agents/openai.yaml +5 -0
- package/.agents/skills/grill/SKILL.md +5 -1
- package/.agents/skills/grill/agents/openai.yaml +5 -0
- package/.agents/skills/harness-audit/SKILL.md +5 -1
- package/.agents/skills/harness-audit/agents/openai.yaml +5 -0
- package/.agents/skills/hook-development/SKILL.md +3 -1
- package/.agents/skills/mcp-builder/SKILL.md +3 -1
- package/.agents/skills/memory-cleanup/SKILL.md +5 -1
- package/.agents/skills/memory-cleanup/agents/openai.yaml +5 -0
- package/.agents/skills/mode-selector/SKILL.md +3 -1
- package/.agents/skills/npm-publish/SKILL.md +4 -2
- package/.agents/skills/peekaboo-driver/SKILL.md +3 -1
- package/.agents/skills/persona-panel/SKILL.md +5 -1
- package/.agents/skills/persona-panel/agents/openai.yaml +5 -0
- package/.agents/skills/plan/SKILL.md +5 -1
- package/.agents/skills/plan/agents/openai.yaml +5 -0
- package/.agents/skills/playwright-driver/SKILL.md +3 -1
- package/.agents/skills/portfolio/SKILL.md +5 -1
- package/.agents/skills/portfolio/agents/openai.yaml +5 -0
- package/.agents/skills/quality-gates/SKILL.md +3 -1
- package/.agents/skills/reconcile/SKILL.md +5 -1
- package/.agents/skills/reconcile/agents/openai.yaml +5 -0
- package/.agents/skills/release/SKILL.md +5 -1
- package/.agents/skills/release/agents/openai.yaml +5 -0
- package/.agents/skills/remote-offload/SKILL.md +3 -1
- package/.agents/skills/repo-audit/SKILL.md +5 -1
- package/.agents/skills/repo-audit/agents/openai.yaml +5 -0
- package/.agents/skills/session/SKILL.md +21 -0
- package/.agents/skills/session/agents/openai.yaml +5 -0
- package/.agents/skills/session-end/SKILL.md +3 -1
- package/.agents/skills/session-plan/SKILL.md +3 -1
- package/.agents/skills/session-start/SKILL.md +3 -1
- package/.agents/skills/spinout/SKILL.md +5 -1
- package/.agents/skills/spinout/agents/openai.yaml +5 -0
- package/.agents/skills/sunset-review/SKILL.md +5 -1
- package/.agents/skills/sunset-review/agents/openai.yaml +5 -0
- package/.agents/skills/templates-ack/SKILL.md +21 -0
- package/.agents/skills/templates-ack/agents/openai.yaml +5 -0
- package/.agents/skills/test/SKILL.md +5 -1
- package/.agents/skills/test/agents/openai.yaml +5 -0
- package/.agents/skills/test-runner/SKILL.md +3 -1
- package/.agents/skills/tmux-layout/SKILL.md +3 -1
- package/.agents/skills/using-orchestrator/SKILL.md +3 -1
- package/.agents/skills/ux-grill/SKILL.md +5 -1
- package/.agents/skills/ux-grill/agents/openai.yaml +5 -0
- package/.agents/skills/vault-mirror/SKILL.md +3 -1
- package/.agents/skills/vault-sync/SKILL.md +3 -1
- package/.agents/skills/wave-executor/SKILL.md +3 -1
- package/.agents/skills/write-executable-plan/SKILL.md +3 -1
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +4 -4
- package/.codex-plugin/skills/convergence-monitoring/SKILL.md +1 -3
- package/.codex-plugin/skills/eval/SKILL.md +1 -1
- package/.codex-plugin/skills/evolve/SKILL.md +1 -1
- package/.codex-plugin/skills/npm-publish/SKILL.md +1 -3
- package/.codex-plugin/skills/session/SKILL.md +1 -1
- package/.cursor/commands/eval.md +1 -1
- package/.cursor/commands/session.md +1 -1
- package/.cursor/rules/000-session-orchestrator.mdc +0 -2
- package/.cursor/rules/050-plan.mdc +1 -1
- package/.cursor/skills/convergence-monitoring/SKILL.md +1 -0
- package/.cursor/skills/eval/SKILL.md +1 -1
- package/.cursor/skills/npm-publish/SKILL.md +1 -0
- package/.cursor-plugin/plugin.json +1 -1
- package/.orchestrator/policy/blocked-commands.json +12 -3
- package/AGENTS.md +3 -2
- package/CHANGELOG.md +136 -0
- package/README.md +9 -9
- package/SECURITY.md +12 -0
- package/agents/dialectic-deriver.md +13 -10
- package/agents/eval-judge.md +67 -45
- package/agents/skill-applied-judge.md +34 -19
- package/commands/session.md +7 -3
- package/docs/baseline.md +12 -6
- package/docs/codex-setup.md +14 -2
- package/docs/components.md +7 -5
- package/docs/events-schema.md +56 -9
- package/docs/rule-authoring.md +58 -6
- package/docs/session-config-reference.md +100 -7
- package/docs/session-config-template.md +31 -2
- package/docs/telemetry.md +2 -0
- package/hooks/_lib/hook-import-set.json +85 -8
- package/hooks/_lib/subagent-transcript.mjs +582 -31
- package/hooks/config-protection.mjs +11 -3
- package/hooks/cwd-change-restore.mjs +11 -3
- package/hooks/enforce-commands.mjs +70 -23
- package/hooks/enforce-scope.mjs +143 -33
- package/hooks/hooks-codex.json +1 -1
- package/hooks/hooks.json +1 -1
- package/hooks/loop-guard.mjs +11 -3
- package/hooks/on-session-end.mjs +58 -23
- package/hooks/on-session-start.mjs +48 -11
- package/hooks/on-stop.mjs +168 -22
- package/hooks/operator-steer.mjs +11 -3
- package/hooks/post-bash-issue-budget-refund.mjs +18 -8
- package/hooks/post-bash-write-verify.mjs +3 -2
- package/hooks/post-edit-import-probe.mjs +17 -9
- package/hooks/post-edit-validate.mjs +13 -5
- package/hooks/post-subagent-discovery-validator.mjs +98 -13
- package/hooks/post-tool-batch-wave-signal.mjs +200 -38
- package/hooks/post-tool-failure-corrective-context.mjs +11 -5
- package/hooks/post-tooluse-frontend-slop.mjs +10 -4
- package/hooks/pre-auq-clarity.mjs +15 -2
- package/hooks/pre-bash-destructive-guard.mjs +80 -9
- package/hooks/pre-bash-issue-budget.mjs +16 -11
- package/hooks/pre-bash-memory-propose-audit.mjs +86 -54
- package/hooks/pre-bash-sessions-ledger-guard.mjs +391 -20
- package/hooks/pre-bash-staging-fence.mjs +335 -31
- package/hooks/pre-bash-templates-first.mjs +19 -14
- package/hooks/pre-task-scope-disjoint.mjs +233 -2
- package/hooks/subagent-telemetry.mjs +15 -19
- package/hooks/wave-scope-commit-guard.mjs +197 -100
- package/monitors/monitors.json +1 -1
- package/output-styles/wave-summary.md +1 -1
- package/package.json +1 -1
- package/pi/prompts/eval.md +1 -1
- package/pi/prompts/session.md +1 -1
- package/rules/README.md +1 -1
- package/rules/opt-in-domain/prompt-caching.md +1 -1
- package/rules/opt-in-stack/backend-data.md +1 -1
- package/rules/opt-in-stack/backend.md +3 -3
- package/rules/opt-in-stack/frontend.md +1 -1
- package/rules/opt-in-stack/security-web.md +3 -3
- package/rules/opt-in-stack/swift.md +1 -1
- package/scripts/autopilot.mjs +23 -2
- package/scripts/backfill-abandoned-sessions.mjs +117 -15
- package/scripts/check-sessions-integrity.mjs +300 -0
- package/scripts/dialectic-deriver.mjs +50 -13
- package/scripts/emit-session.mjs +75 -29
- package/scripts/eval-session.mjs +65 -3
- package/scripts/generate-agents-skills.mjs +102 -29
- package/scripts/generate-cursor-adapter.mjs +61 -16
- package/scripts/lib/agent-status.mjs +2 -31
- package/scripts/lib/auq/clarity.mjs +10 -2
- package/scripts/lib/auq/parse.mjs +12 -31
- package/scripts/lib/auq/schema.mjs +56 -41
- package/scripts/lib/auto-dialectic.mjs +304 -15
- package/scripts/lib/autopilot/flags.mjs +12 -1
- package/scripts/lib/autopilot/kill-switches.mjs +6 -3
- package/scripts/lib/autopilot/loop.mjs +14 -1
- package/scripts/lib/autopilot/stall-sampler.mjs +80 -23
- package/scripts/lib/ci-status-banner.mjs +376 -16
- package/scripts/lib/command-blocker.mjs +275 -28
- package/scripts/lib/config/dialectic.mjs +12 -3
- package/scripts/lib/config/gate.mjs +74 -0
- package/scripts/lib/config/reaper.mjs +162 -0
- package/scripts/lib/config.mjs +14 -0
- package/scripts/lib/convergence-monitor.mjs +74 -11
- package/scripts/lib/ecosystem-health.mjs +11 -0
- package/scripts/lib/eval/engine.mjs +421 -53
- package/scripts/lib/eval/judge.mjs +463 -40
- package/scripts/lib/eval/schema.mjs +10 -1
- package/scripts/lib/events-rotation.mjs +221 -25
- package/scripts/lib/events-schema.mjs +114 -0
- package/scripts/lib/events.mjs +524 -5
- package/scripts/lib/frontmatter-guard.mjs +21 -10
- package/scripts/lib/gates/gate-baseline.mjs +27 -2
- package/scripts/lib/gates/gate-full.mjs +28 -3
- package/scripts/lib/gates/gate-helpers.mjs +243 -21
- package/scripts/lib/gates/gate-incremental.mjs +28 -3
- package/scripts/lib/gates/gate-per-file.mjs +27 -2
- package/scripts/lib/gitlab-portfolio/markdown-writer.mjs +6 -1
- package/scripts/lib/instruction-budget-guard.mjs +146 -4
- package/scripts/lib/io.mjs +42 -8
- package/scripts/lib/issue-close-strip-labels.mjs +207 -49
- package/scripts/lib/js-mask.mjs +197 -0
- package/scripts/lib/learnings/evolve-telemetry.mjs +11 -7
- package/scripts/lib/maintenance-due-banner.mjs +53 -88
- package/scripts/lib/orphan-reaper.mjs +1588 -0
- package/scripts/lib/peer-cards/merger.mjs +48 -10
- package/scripts/lib/peer-cards/reader.mjs +78 -2
- package/scripts/lib/process-group.mjs +899 -0
- package/scripts/lib/quality-gate.mjs +107 -28
- package/scripts/lib/reconcile/backlog.mjs +368 -0
- package/scripts/lib/reconcile/engine.mjs +55 -188
- package/scripts/lib/reconcile/rule-expiry-sweep.mjs +302 -60
- package/scripts/lib/reconcile/sanitize.mjs +69 -3
- package/scripts/lib/reconcile-nudge-banner.mjs +138 -45
- package/scripts/lib/resource-probe/parsers.mjs +31 -0
- package/scripts/lib/rule-loader.mjs +41 -12
- package/scripts/lib/scope-echo.mjs +39 -2
- package/scripts/lib/scope-gate.mjs +605 -1
- package/scripts/lib/session-close-backfill.mjs +33 -6
- package/scripts/lib/session-id.mjs +9 -20
- package/scripts/lib/session-invocation.mjs +20 -0
- package/scripts/lib/session-schema/constants.mjs +30 -2
- package/scripts/lib/session-schema/normalizer.mjs +56 -4
- package/scripts/lib/session-schema.mjs +8 -3
- package/scripts/lib/session-start-probes.mjs +95 -10
- package/scripts/lib/sessions-canonical.mjs +23 -0
- package/scripts/lib/sessions-integrity-banner.mjs +7 -1
- package/scripts/lib/sessions-staleness-banner.mjs +193 -51
- package/scripts/lib/skill-evidence-window.mjs +891 -0
- package/scripts/lib/skill-evolution/candidate-intake.mjs +133 -12
- package/scripts/lib/skill-evolution/engine.mjs +18 -9
- package/scripts/lib/skill-judge.mjs +45 -3
- package/scripts/lib/tail-window.mjs +56 -0
- package/scripts/lib/telemetry/schema.mjs +30 -0
- package/scripts/lib/telemetry/sync.mjs +61 -6
- package/scripts/lib/telemetry-flush-health-banner.mjs +4 -22
- package/scripts/lib/test-runner/issue-reconcile.mjs +48 -16
- package/scripts/lib/tmux-layout/telemetry-stats.mjs +72 -13
- package/scripts/lib/user-invocable-skills.mjs +23 -3
- package/scripts/lib/ux-grill/reconcile.mjs +48 -22
- package/scripts/lib/validate/check-agents-skills.mjs +26 -15
- package/scripts/lib/validate/check-cursor-adapter.mjs +1 -0
- package/scripts/lib/validate/check-entry-guard.mjs +13 -50
- package/scripts/lib/validate/check-hook-entry-guards.mjs +636 -0
- package/scripts/lib/validate/check-pi-prompts.mjs +1 -0
- package/scripts/lib/validate/check-rules.mjs +7 -5
- package/scripts/lib/validate/check-skill-links.mjs +9 -1
- package/scripts/lib/validate/check-skill-script-paths.mjs +239 -27
- package/scripts/lib/validate/check-test-git-config-target.mjs +24 -34
- package/scripts/lib/validate/check-untracked-test-deps.mjs +7 -102
- package/scripts/lib/validate/check-unwired-features.mjs +130 -27
- package/scripts/lib/validate/check-validator-registration.mjs +34 -10
- package/scripts/lib/validate/confidential-names.mjs +10 -0
- package/scripts/lib/validate-vendored-rules.mjs +4 -3
- package/scripts/lib/vault-mirror/namespace.mjs +46 -8
- package/scripts/lib/vault-mirror/process.mjs +10 -3
- package/scripts/lib/vault-mirror/render-sessions.mjs +12 -2
- package/scripts/lib/vault-status/narrative-mirror.mjs +31 -7
- package/scripts/lib/vault-yaml.mjs +118 -0
- package/scripts/lib/worktree/lifecycle.mjs +153 -1
- package/scripts/release-session-lock.mjs +305 -0
- package/scripts/release.mjs +30 -5
- package/scripts/resolve-session-invocation.mjs +59 -0
- package/scripts/run-quality-gate.mjs +156 -17
- package/scripts/sweep-expired-rules.mjs +14 -3
- package/scripts/validate-plugin.mjs +12 -0
- package/scripts/validate-wave-scope.mjs +32 -105
- package/scripts/vault-mirror.mjs +9 -1
- package/skills/_shared/platform-tools.md +23 -11
- package/skills/autopilot/SKILL.md +22 -7
- package/skills/claude-md-drift-check/SKILL.md +1 -1
- package/skills/convergence-monitoring/README.md +8 -1
- package/skills/convergence-monitoring/SIGNALS.md +50 -6
- package/skills/convergence-monitoring/SKILL.md +15 -6
- package/skills/eval/SKILL.md +39 -24
- package/skills/eval/rubric-v1.md +1 -0
- package/skills/eval/rubric-v2.md +457 -0
- package/skills/evolve/SKILL.md +1 -1
- package/skills/evolve/references/evolve-dialectic-mode.md +42 -25
- package/skills/gitlab-ops/SKILL.md +3 -2
- package/skills/npm-publish/SKILL.md +1 -1
- package/skills/reconcile/SKILL.md +11 -0
- package/skills/session-end/SKILL.md +13 -16
- package/skills/session-end/discovery-scan.md +1 -1
- package/skills/session-end/phase-3-6-tail.md +55 -9
- package/skills/session-end/references/phase-5-issue-cleanup.md +9 -14
- package/skills/session-end/session-metrics-write.md +10 -0
- package/skills/session-plan/SKILL.md +17 -5
- package/skills/session-plan/references/session-plan-task-classification.md +2 -2
- package/skills/session-start/references/phase-4-ssot-environment-check.md +2 -1
- package/skills/ux-grill/SKILL.md +1 -1
- package/skills/wave-executor/SKILL.md +8 -4
- package/skills/wave-executor/circuit-breaker.md +2 -0
- package/skills/wave-executor/references/wave-executor-state-init.md +5 -3
- package/skills/wave-executor/references/wave-loop-dispatch.md +2 -1
- package/.codex-plugin/skills/convergence-monitoring/agents/openai.yaml +0 -5
- package/.codex-plugin/skills/npm-publish/agents/openai.yaml +0 -5
- package/.cursor/commands/convergence-monitoring.md +0 -13
- package/.cursor/commands/npm-publish.md +0 -13
- package/pi/prompts/convergence-monitoring.md +0 -11
- package/pi/prompts/npm-publish.md +0 -11
package/scripts/lib/io.mjs
CHANGED
|
@@ -711,12 +711,33 @@ export function emitRewrite(updatedInput) {
|
|
|
711
711
|
process.exit(0);
|
|
712
712
|
};
|
|
713
713
|
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
714
|
+
// `updatedInput` is a MAP in the bundle schema, so an array is as wrong as a
|
|
715
|
+
// string — and an array would serialize into a shape the harness cannot use.
|
|
716
|
+
//
|
|
717
|
+
// PLAIN object only, and `typeof === 'object'` is not that test. An object
|
|
718
|
+
// WRAPPER passes it and then serializes to something else entirely:
|
|
719
|
+
// `new Date()` and `new String('x')` become JSON STRINGS (their `toJSON` /
|
|
720
|
+
// primitive form), `new Map([...])` becomes `{}` — which looks valid and,
|
|
721
|
+
// per the doc above, deletes every field of the tool input. Each of the three
|
|
722
|
+
// reaches the harness as a schema-invalid `updatedInput`, and the bundle
|
|
723
|
+
// turns that into `behavior: "deny"`: on the `AskUserQuestion` path the
|
|
724
|
+
// operator's question is destroyed to save a rewrite. A prototype of
|
|
725
|
+
// `Object.prototype` (a literal) or `null` (`Object.create(null)`) is the
|
|
726
|
+
// whole admissible set; everything else takes the same no-op-plus-stderr
|
|
727
|
+
// contract as `null`, an array or a string.
|
|
728
|
+
const proto =
|
|
729
|
+
updatedInput !== null && typeof updatedInput === 'object'
|
|
730
|
+
? Object.getPrototypeOf(updatedInput)
|
|
731
|
+
: undefined;
|
|
732
|
+
const isPlainObject =
|
|
733
|
+
!Array.isArray(updatedInput) && (proto === Object.prototype || proto === null);
|
|
734
|
+
if (!isPlainObject) {
|
|
735
|
+
let described;
|
|
736
|
+
if (Array.isArray(updatedInput)) described = 'an array';
|
|
737
|
+
else if (updatedInput === null) described = 'null';
|
|
738
|
+
else if (typeof updatedInput !== 'object') described = String(typeof updatedInput);
|
|
739
|
+
else described = `a non-plain object (${proto?.constructor?.name ?? 'unknown prototype'})`;
|
|
740
|
+
bail(`was called with ${described}, not a tool-input object`);
|
|
720
741
|
}
|
|
721
742
|
|
|
722
743
|
let line;
|
|
@@ -731,11 +752,24 @@ export function emitRewrite(updatedInput) {
|
|
|
731
752
|
},
|
|
732
753
|
});
|
|
733
754
|
} catch (err) {
|
|
734
|
-
// A cycle or a BigInt in the caller's object.
|
|
735
|
-
// undefined (a toJSON that yields undefined) — caught by the same guard.
|
|
755
|
+
// A cycle or a BigInt in the caller's object.
|
|
736
756
|
bail(`could not serialize the tool input (${err?.message ?? String(err)})`);
|
|
737
757
|
}
|
|
738
758
|
|
|
759
|
+
// `JSON.stringify` returns `undefined` only for a TOP-LEVEL value it drops —
|
|
760
|
+
// and `updatedInput` sits NESTED inside the envelope literal above, so at this
|
|
761
|
+
// call site it never can. Measured 2026-09-18 with
|
|
762
|
+
// `{ toJSON() { return undefined; } }` as `updatedInput`: stdout is
|
|
763
|
+
// `{"hookSpecificOutput":{"hookEventName":"PreToolUse"}}` (53 bytes) — the
|
|
764
|
+
// property is silently OMITTED, not undefined, and the guard below does not
|
|
765
|
+
// fire. That degraded state is the safe one (an envelope without
|
|
766
|
+
// `updatedInput` carries no rewrite, so the tool call runs with its original
|
|
767
|
+
// input — exactly what `bail` produces), which is why it is documented rather
|
|
768
|
+
// than checked: re-parsing up to REWRITE_ENVELOPE_MAX_BYTES on every rewrite
|
|
769
|
+
// to recover one stderr line is a poor trade. The guard stays because it is
|
|
770
|
+
// the envelope SHAPE that makes the case unreachable: hoist `updatedInput` to
|
|
771
|
+
// the top level, or hand the literal to a helper that returns it, and
|
|
772
|
+
// `undefined` becomes reachable again.
|
|
739
773
|
if (typeof line !== 'string') {
|
|
740
774
|
bail('serialized the tool input to undefined (a toJSON returning undefined?)');
|
|
741
775
|
}
|
|
@@ -20,19 +20,24 @@
|
|
|
20
20
|
* resolve to the wrong GitLab instance on a multi-host machine, turning
|
|
21
21
|
* this whole module into a silent no-op.
|
|
22
22
|
*
|
|
23
|
-
* Usage (
|
|
23
|
+
* Usage (session-end Phase 5 issue-close step) — ONE command strips, closes
|
|
24
|
+
* and verifies every resolved issue:
|
|
24
25
|
*
|
|
25
|
-
*
|
|
26
|
+
* node scripts/lib/issue-close-strip-labels.mjs --close --vcs gitlab 1388 1387
|
|
26
27
|
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
28
|
+
* It prints one JSON line per id and exits 1 when any id did not verify as
|
|
29
|
+
* closed. The mechanical call exists because the earlier prose-only step
|
|
30
|
+
* ("call stripStatusLabels, then close") was skipped often enough that 339
|
|
31
|
+
* closed issues still carried `status:in-progress` (measured 2026-09-19).
|
|
32
|
+
* `closeIssues()` is the same sequence as a library call; `stripStatusLabels()`
|
|
33
|
+
* stays exported for callers that only strip.
|
|
31
34
|
*
|
|
32
35
|
* Auto-created by `scripts/lib/issue-close-strip-labels.mjs` (#308).
|
|
33
36
|
*/
|
|
34
37
|
|
|
35
38
|
import { execFileSync } from 'node:child_process';
|
|
39
|
+
import { parseArgs } from 'node:util';
|
|
40
|
+
import { isMainModule } from './is-main-module.mjs';
|
|
36
41
|
import { resolveRepoSpec } from './vcs-repo-spec.mjs';
|
|
37
42
|
|
|
38
43
|
/** Regex that matches any `status:*` label name. */
|
|
@@ -43,11 +48,12 @@ const STATUS_LABEL_RE = /^status:/;
|
|
|
43
48
|
*
|
|
44
49
|
* @param {string} cmd
|
|
45
50
|
* @param {string[]} args
|
|
51
|
+
* @param {typeof execFileSync} [execFile] injectable executor (tests)
|
|
46
52
|
* @returns {{ ok: boolean, stdout: string, stderr: string }}
|
|
47
53
|
*/
|
|
48
|
-
function runCli(cmd, args) {
|
|
54
|
+
function runCli(cmd, args, execFile = execFileSync) {
|
|
49
55
|
try {
|
|
50
|
-
const stdout =
|
|
56
|
+
const stdout = execFile(cmd, args, {
|
|
51
57
|
encoding: 'utf8',
|
|
52
58
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
53
59
|
});
|
|
@@ -61,50 +67,50 @@ function runCli(cmd, args) {
|
|
|
61
67
|
}
|
|
62
68
|
|
|
63
69
|
/**
|
|
64
|
-
*
|
|
70
|
+
* Read an issue as JSON via `glab issue view --output json` (every field) or
|
|
71
|
+
* `gh issue view --json <ghFields>` (only the named fields).
|
|
65
72
|
*
|
|
66
|
-
* @param {{ issueId:
|
|
73
|
+
* @param {{ issueId: string, vcs: 'gitlab' | 'github', spec?: string,
|
|
74
|
+
* ghFields: string, execFile?: typeof execFileSync }} opts
|
|
67
75
|
* `spec` is the resolved `-R`/`--repo` host-pinning value (#839); omitted
|
|
68
76
|
* (undefined) means no `-R` is appended — never emit `-R undefined`.
|
|
69
|
-
* @returns {{ ok: boolean,
|
|
77
|
+
* @returns {{ ok: boolean, data: any, stderr: string }}
|
|
70
78
|
*/
|
|
71
|
-
function
|
|
72
|
-
const
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
const args = ['issue', 'view', id, '--json', 'labels'];
|
|
77
|
-
if (spec) args.push('-R', spec);
|
|
78
|
-
const res = runCli('gh', args);
|
|
79
|
-
if (!res.ok) return { ok: false, labels: [], stderr: res.stderr };
|
|
80
|
-
let parsed;
|
|
81
|
-
try {
|
|
82
|
-
parsed = JSON.parse(res.stdout);
|
|
83
|
-
} catch (e) {
|
|
84
|
-
return { ok: false, labels: [], stderr: `JSON parse failed: ${e && e.message ? e.message : String(e)}` };
|
|
85
|
-
}
|
|
86
|
-
const labels = Array.isArray(parsed?.labels)
|
|
87
|
-
? parsed.labels.map((l) => (typeof l?.name === 'string' ? l.name : String(l))).filter(Boolean)
|
|
88
|
-
: [];
|
|
89
|
-
return { ok: true, labels, stderr: '' };
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
// Default: gitlab via glab.
|
|
93
|
-
// `glab issue view <IID> --output json` returns an issue object with a `labels` array.
|
|
94
|
-
const args = ['issue', 'view', id, '--output', 'json'];
|
|
79
|
+
function viewIssueJson({ issueId, vcs, spec, ghFields, execFile }) {
|
|
80
|
+
const args =
|
|
81
|
+
vcs === 'github'
|
|
82
|
+
? ['issue', 'view', issueId, '--json', ghFields]
|
|
83
|
+
: ['issue', 'view', issueId, '--output', 'json'];
|
|
95
84
|
if (spec) args.push('-R', spec);
|
|
96
|
-
const res = runCli('glab', args);
|
|
97
|
-
if (!res.ok) return { ok: false,
|
|
98
|
-
let parsed;
|
|
85
|
+
const res = runCli(vcs === 'github' ? 'gh' : 'glab', args, execFile);
|
|
86
|
+
if (!res.ok) return { ok: false, data: null, stderr: res.stderr };
|
|
99
87
|
try {
|
|
100
|
-
|
|
88
|
+
return { ok: true, data: JSON.parse(res.stdout), stderr: '' };
|
|
101
89
|
} catch (e) {
|
|
102
|
-
return {
|
|
90
|
+
return {
|
|
91
|
+
ok: false,
|
|
92
|
+
data: null,
|
|
93
|
+
stderr: `JSON parse failed: ${e && e.message ? e.message : String(e)}`,
|
|
94
|
+
};
|
|
103
95
|
}
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Fetch the label names currently applied to an issue.
|
|
100
|
+
*
|
|
101
|
+
* GitHub shape: `{ labels: [{ name }] }`; GitLab shape: `{ labels: ["status:ready", ...] }`.
|
|
102
|
+
*
|
|
103
|
+
* @param {{ issueId: string, vcs: 'gitlab' | 'github', spec?: string, execFile?: typeof execFileSync }} opts
|
|
104
|
+
* @returns {{ ok: boolean, labels: string[], stderr: string }}
|
|
105
|
+
*/
|
|
106
|
+
function fetchLabels({ issueId, vcs, spec, execFile }) {
|
|
107
|
+
const { ok, data, stderr } = viewIssueJson({ issueId, vcs, spec, ghFields: 'labels', execFile });
|
|
108
|
+
if (!ok) return { ok: false, labels: [], stderr };
|
|
109
|
+
const toName =
|
|
110
|
+
vcs === 'github'
|
|
111
|
+
? (l) => (typeof l?.name === 'string' ? l.name : String(l))
|
|
112
|
+
: (l) => (typeof l === 'string' ? l : String(l));
|
|
113
|
+
const labels = Array.isArray(data?.labels) ? data.labels.map(toName).filter(Boolean) : [];
|
|
108
114
|
return { ok: true, labels, stderr: '' };
|
|
109
115
|
}
|
|
110
116
|
|
|
@@ -119,12 +125,14 @@ function fetchLabels({ issueId, vcs, spec }) {
|
|
|
119
125
|
* issueId: number | string,
|
|
120
126
|
* vcs?: 'gitlab' | 'github',
|
|
121
127
|
* repoRoot?: string,
|
|
122
|
-
* resolveRepoSpecFn?: (opts: { repoRoot: string, vcs: 'gitlab' | 'github' }) => string | undefined
|
|
128
|
+
* resolveRepoSpecFn?: (opts: { repoRoot: string, vcs: 'gitlab' | 'github' }) => string | undefined,
|
|
129
|
+
* execFile?: typeof execFileSync
|
|
123
130
|
* }} opts
|
|
124
131
|
* `repoRoot` defaults to `process.cwd()`. `resolveRepoSpecFn` is the
|
|
125
132
|
* injectable seam for the `-R`/`--repo` host-pinning resolution (#839) —
|
|
126
133
|
* defaults to the real `resolveRepoSpec` (shells out to `git remote
|
|
127
|
-
* get-url`); tests inject a stub instead of shelling out.
|
|
134
|
+
* get-url`); tests inject a stub instead of shelling out. `execFile`
|
|
135
|
+
* (default `execFileSync`) runs every glab/gh call.
|
|
128
136
|
* @returns {Promise<{ stripped: string[], error?: string }>}
|
|
129
137
|
*/
|
|
130
138
|
export async function stripStatusLabels({
|
|
@@ -132,6 +140,7 @@ export async function stripStatusLabels({
|
|
|
132
140
|
vcs = 'gitlab',
|
|
133
141
|
repoRoot = process.cwd(),
|
|
134
142
|
resolveRepoSpecFn = resolveRepoSpec,
|
|
143
|
+
execFile = execFileSync,
|
|
135
144
|
} = {}) {
|
|
136
145
|
try {
|
|
137
146
|
const id = String(issueId ?? '').trim();
|
|
@@ -146,7 +155,7 @@ export async function stripStatusLabels({
|
|
|
146
155
|
const spec = resolveRepoSpecFn({ repoRoot, vcs: vcsResolved });
|
|
147
156
|
|
|
148
157
|
// Step 1: fetch current labels.
|
|
149
|
-
const { ok, labels, stderr } = fetchLabels({ issueId: id, vcs: vcsResolved, spec });
|
|
158
|
+
const { ok, labels, stderr } = fetchLabels({ issueId: id, vcs: vcsResolved, spec, execFile });
|
|
150
159
|
if (!ok) {
|
|
151
160
|
return { stripped: [], error: `failed to fetch labels: ${stderr}` };
|
|
152
161
|
}
|
|
@@ -167,13 +176,13 @@ export async function stripStatusLabels({
|
|
|
167
176
|
args.push('--remove-label', label);
|
|
168
177
|
}
|
|
169
178
|
if (spec) args.push('-R', spec);
|
|
170
|
-
stripRes = runCli('gh', args);
|
|
179
|
+
stripRes = runCli('gh', args, execFile);
|
|
171
180
|
} else {
|
|
172
181
|
// `glab issue update <IID> --unlabel label1,label2,...`
|
|
173
182
|
// glab accepts comma-separated list in a single --unlabel flag value.
|
|
174
183
|
const args = ['issue', 'update', id, '--unlabel', toStrip.join(',')];
|
|
175
184
|
if (spec) args.push('-R', spec);
|
|
176
|
-
stripRes = runCli('glab', args);
|
|
185
|
+
stripRes = runCli('glab', args, execFile);
|
|
177
186
|
}
|
|
178
187
|
|
|
179
188
|
if (!stripRes.ok) {
|
|
@@ -189,3 +198,152 @@ export async function stripStatusLabels({
|
|
|
189
198
|
return { stripped: [], error: msg };
|
|
190
199
|
}
|
|
191
200
|
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Strip `status:*` labels, close, and VERIFY the close for each issue id — in
|
|
204
|
+
* that order, one id at a time.
|
|
205
|
+
*
|
|
206
|
+
* `closed` is true only when a re-read of the issue AFTER the close says
|
|
207
|
+
* `state: closed` (GitLab `closed`, GitHub `CLOSED`). The close command's own
|
|
208
|
+
* exit status is not trusted: a wrong project or a silent 404 can exit 0 while
|
|
209
|
+
* closing nothing (gitlab-ops skill, "Close verification"). The re-read runs
|
|
210
|
+
* even when the close command failed, so an issue that was already closed
|
|
211
|
+
* reports `closed: true` alongside the close `error`.
|
|
212
|
+
*
|
|
213
|
+
* A strip failure is non-fatal: it is reported as `stripError` and the close
|
|
214
|
+
* still runs. An id that is not a positive integer (an optional leading `#` is
|
|
215
|
+
* accepted) gets no CLI call at all — a value starting with `-` would
|
|
216
|
+
* otherwise reach glab/gh as a flag.
|
|
217
|
+
*
|
|
218
|
+
* @param {{
|
|
219
|
+
* ids: Array<number | string>,
|
|
220
|
+
* vcs?: 'gitlab' | 'github',
|
|
221
|
+
* repo?: string,
|
|
222
|
+
* repoRoot?: string,
|
|
223
|
+
* resolveRepoSpecFn?: (opts: { repoRoot: string, vcs: 'gitlab' | 'github' }) => string | undefined,
|
|
224
|
+
* execFile?: typeof execFileSync
|
|
225
|
+
* }} opts
|
|
226
|
+
* `repo` overrides the `-R`/`--repo` spec; otherwise it is resolved once via
|
|
227
|
+
* `resolveRepoSpecFn` exactly as `stripStatusLabels` does (#839).
|
|
228
|
+
* @returns {Promise<Array<{ id: string, stripped: string[], stripError?: string,
|
|
229
|
+
* closed: boolean, state: string | null, error?: string }>>}
|
|
230
|
+
*/
|
|
231
|
+
export async function closeIssues({
|
|
232
|
+
ids = [],
|
|
233
|
+
vcs = 'gitlab',
|
|
234
|
+
repo,
|
|
235
|
+
repoRoot = process.cwd(),
|
|
236
|
+
resolveRepoSpecFn = resolveRepoSpec,
|
|
237
|
+
execFile = execFileSync,
|
|
238
|
+
} = {}) {
|
|
239
|
+
const vcsResolved = vcs === 'github' ? 'github' : 'gitlab';
|
|
240
|
+
const spec = repo?.trim() || resolveRepoSpecFn({ repoRoot, vcs: vcsResolved });
|
|
241
|
+
const cli = vcsResolved === 'github' ? 'gh' : 'glab';
|
|
242
|
+
const results = [];
|
|
243
|
+
|
|
244
|
+
for (const rawId of ids) {
|
|
245
|
+
const id = String(rawId ?? '')
|
|
246
|
+
.trim()
|
|
247
|
+
.replace(/^#/, '');
|
|
248
|
+
if (!/^[1-9]\d*$/.test(id)) {
|
|
249
|
+
results.push({
|
|
250
|
+
id: String(rawId),
|
|
251
|
+
stripped: [],
|
|
252
|
+
closed: false,
|
|
253
|
+
state: null,
|
|
254
|
+
error: 'invalid issue id',
|
|
255
|
+
});
|
|
256
|
+
continue;
|
|
257
|
+
}
|
|
258
|
+
const result = { id, stripped: [], closed: false, state: null };
|
|
259
|
+
|
|
260
|
+
// 1. strip — non-fatal. The resolved spec is handed through so strip,
|
|
261
|
+
// close and verify all pin the same repo.
|
|
262
|
+
const strip = await stripStatusLabels({
|
|
263
|
+
issueId: id,
|
|
264
|
+
vcs: vcsResolved,
|
|
265
|
+
resolveRepoSpecFn: () => spec,
|
|
266
|
+
execFile,
|
|
267
|
+
});
|
|
268
|
+
result.stripped = strip.stripped;
|
|
269
|
+
if (strip.error) result.stripError = strip.error;
|
|
270
|
+
|
|
271
|
+
// 2. close
|
|
272
|
+
const closeArgs = ['issue', 'close', id];
|
|
273
|
+
if (spec) closeArgs.push('-R', spec);
|
|
274
|
+
const closeRes = runCli(cli, closeArgs, execFile);
|
|
275
|
+
const errors = closeRes.ok
|
|
276
|
+
? []
|
|
277
|
+
: [`close failed: ${closeRes.stderr.trim() || 'unknown error'}`];
|
|
278
|
+
|
|
279
|
+
// 3. verify — the platform's state is the only evidence of a close.
|
|
280
|
+
const view = viewIssueJson({
|
|
281
|
+
issueId: id,
|
|
282
|
+
vcs: vcsResolved,
|
|
283
|
+
spec,
|
|
284
|
+
ghFields: 'state',
|
|
285
|
+
execFile,
|
|
286
|
+
});
|
|
287
|
+
if (view.ok) {
|
|
288
|
+
result.state = typeof view.data?.state === 'string' ? view.data.state : null;
|
|
289
|
+
result.closed = result.state?.toLowerCase() === 'closed';
|
|
290
|
+
if (!result.closed) {
|
|
291
|
+
errors.push(`verify: issue state is ${result.state ?? 'unknown'}, not closed`);
|
|
292
|
+
}
|
|
293
|
+
} else {
|
|
294
|
+
errors.push(`verify failed: ${view.stderr.trim() || 'unknown error'}`);
|
|
295
|
+
}
|
|
296
|
+
if (errors.length) result.error = errors.join('; ');
|
|
297
|
+
results.push(result);
|
|
298
|
+
}
|
|
299
|
+
return results;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
const USAGE =
|
|
303
|
+
'usage: issue-close-strip-labels.mjs --close [--vcs gitlab|github] [-R|--repo <spec>] <id>...';
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* CLI: `--close [--vcs gitlab|github] [-R <spec>] <id>...` — one JSON line per
|
|
307
|
+
* id on stdout. Exit 0 = every id verified closed, 1 = at least one did not,
|
|
308
|
+
* 2 = usage error (no GitLab/GitHub call made).
|
|
309
|
+
*
|
|
310
|
+
* @param {string[]} [argv]
|
|
311
|
+
* @returns {Promise<number>} exit code
|
|
312
|
+
*/
|
|
313
|
+
async function main(argv = process.argv.slice(2)) {
|
|
314
|
+
let parsed;
|
|
315
|
+
try {
|
|
316
|
+
parsed = parseArgs({
|
|
317
|
+
args: argv,
|
|
318
|
+
allowPositionals: true,
|
|
319
|
+
options: {
|
|
320
|
+
close: { type: 'boolean', default: false },
|
|
321
|
+
vcs: { type: 'string', default: 'gitlab' },
|
|
322
|
+
repo: { type: 'string', short: 'R' },
|
|
323
|
+
},
|
|
324
|
+
});
|
|
325
|
+
} catch (err) {
|
|
326
|
+
process.stderr.write(`${err?.message ?? err}\n${USAGE}\n`);
|
|
327
|
+
return 2;
|
|
328
|
+
}
|
|
329
|
+
const { values, positionals } = parsed;
|
|
330
|
+
if (!values.close || positionals.length === 0 || !['gitlab', 'github'].includes(values.vcs)) {
|
|
331
|
+
process.stderr.write(`${USAGE}\n`);
|
|
332
|
+
return 2;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
const results = await closeIssues({ ids: positionals, vcs: values.vcs, repo: values.repo });
|
|
336
|
+
for (const r of results) {
|
|
337
|
+
if (r.stripError) {
|
|
338
|
+
process.stderr.write(
|
|
339
|
+
`⚠ label strip failed for #${r.id}: ${r.stripError} — close attempted anyway\n`,
|
|
340
|
+
);
|
|
341
|
+
}
|
|
342
|
+
process.stdout.write(`${JSON.stringify(r)}\n`);
|
|
343
|
+
}
|
|
344
|
+
return results.every((r) => r.closed) ? 0 : 1;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
if (isMainModule(import.meta.url)) {
|
|
348
|
+
process.exitCode = await main();
|
|
349
|
+
}
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
// js-mask.mjs — ONE JavaScript lexer for every "blank the comments / separate
|
|
2
|
+
// code from data" scanner in this repo.
|
|
3
|
+
//
|
|
4
|
+
// WHY IT LIVES HERE. `maskSource` grew inside
|
|
5
|
+
// `scripts/lib/validate/check-untracked-test-deps.mjs`, where it is used to
|
|
6
|
+
// separate CODE from DATA. Three OTHER modules had hand-rolled
|
|
7
|
+
// `stripComments` copies with NO regex-literal branch, and each one was
|
|
8
|
+
// measurably wrong for the same input shape (`/\/*$/` — a regex whose body
|
|
9
|
+
// contains `/*`), measured 2026-09-18 @ 20a4cbff:
|
|
10
|
+
// - `check-validator-registration.mjs` — the regex's `/*` opened a block
|
|
11
|
+
// comment that swallowed the rest of the surface text → a FALSE
|
|
12
|
+
// "UNREGISTERED" finding.
|
|
13
|
+
// - `check-test-git-config-target.mjs` — same swallow inside a call tail →
|
|
14
|
+
// `wrapperHasCwd` returned false → a false alarm.
|
|
15
|
+
// - `auq/parse.mjs` — the swallowed region no longer contained
|
|
16
|
+
// `(Recommended)` → a silent FALSE NEGATIVE in the AUQ clarity measurement.
|
|
17
|
+
// One lexer, five consumers: the regex branch and its three named ceilings are
|
|
18
|
+
// maintained once.
|
|
19
|
+
//
|
|
20
|
+
// Import-safety: importing this module executes nothing.
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Keywords after which a `/` opens a REGEX LITERAL, never a division. The
|
|
24
|
+
* single-character `prev` below cannot tell `return /x/` from `n / x`, because
|
|
25
|
+
* the character it sees (`n`) is a word character in both. This set is the
|
|
26
|
+
* one-token lookback that closes the difference; it is deliberately the usual
|
|
27
|
+
* short list rather than a parser (BV-001.6/BV-004 — a full expression parser
|
|
28
|
+
* for a masker is the abstraction this file exists to avoid).
|
|
29
|
+
*
|
|
30
|
+
* Named ceiling (BV-004), in the other direction: a genuine DIVISION CHAIN after
|
|
31
|
+
* one of these words (`return a / b / c`) is now read as a regex and its middle
|
|
32
|
+
* blanked. Measured 2026-09-18 over `git ls-files '*.mjs'` (1245 files): 0
|
|
33
|
+
* occurrences — all 18 files whose masking this lookback changes carry a real
|
|
34
|
+
* `return /…/` literal. REVISIT TRIGGER: the first such division chain.
|
|
35
|
+
*/
|
|
36
|
+
const REGEX_PRECEDING_KEYWORDS = new Set([
|
|
37
|
+
'return', 'typeof', 'case', 'in', 'of', 'new', 'delete', 'void', 'yield',
|
|
38
|
+
'await', 'instanceof', 'do', 'else',
|
|
39
|
+
]);
|
|
40
|
+
|
|
41
|
+
/** Word characters, for the one-token lookback above. */
|
|
42
|
+
const WORD_CHAR_RE = /[A-Za-z0-9_$]/;
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Blank out the INTERIOR of every string literal, template literal, comment and
|
|
46
|
+
* regex literal, preserving byte offsets and newlines. Delimiters are kept so
|
|
47
|
+
* paren/comma balance and identifier boundaries survive.
|
|
48
|
+
*
|
|
49
|
+
* This is what separates CODE from DATA. A test that embeds fixture source in a
|
|
50
|
+
* template literal (`check-untracked-test-deps.mjs`'s own test file does) must
|
|
51
|
+
* not be scanned as though that fixture were its own body — masking makes that
|
|
52
|
+
* structural instead of relying on a self-exemption marker.
|
|
53
|
+
*
|
|
54
|
+
* With `keepLiterals: true` only COMMENTS are blanked: string, template and
|
|
55
|
+
* regex literals are still recognised (so a `//` or `/*` inside one is never read
|
|
56
|
+
* as a comment, and a quote inside a regex never opens a string) but their
|
|
57
|
+
* contents stay intact. `check-entry-guard.mjs` needs exactly that — it must see
|
|
58
|
+
* `endsWith('x.mjs')` and `${process.argv[1]}` verbatim (#1383) — and it is the
|
|
59
|
+
* mode the three `stripComments` consumers delegate in (#1388).
|
|
60
|
+
*
|
|
61
|
+
* @param {string} text
|
|
62
|
+
* @param {{ keepLiterals?: boolean }} [options]
|
|
63
|
+
* @returns {string} same-length masked source
|
|
64
|
+
*/
|
|
65
|
+
export function maskSource(text, { keepLiterals = false } = {}) {
|
|
66
|
+
const out = text.split('');
|
|
67
|
+
const n = out.length;
|
|
68
|
+
let i = 0;
|
|
69
|
+
// Last significant (non-space) char before `i`, and its offset, used for the
|
|
70
|
+
// regex/division disambiguation below (the offset carries the one-token
|
|
71
|
+
// keyword lookback).
|
|
72
|
+
let prev = '';
|
|
73
|
+
let prevIdx = -1;
|
|
74
|
+
|
|
75
|
+
/** The whole word ending at `idx`, or '' when `idx` is not a word char. */
|
|
76
|
+
const wordEndingAt = (idx) => {
|
|
77
|
+
if (idx < 0 || !WORD_CHAR_RE.test(text[idx])) return '';
|
|
78
|
+
let k = idx;
|
|
79
|
+
while (k > 0 && WORD_CHAR_RE.test(text[k - 1])) k--;
|
|
80
|
+
return text.slice(k, idx + 1);
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
const blank = (from, to) => {
|
|
84
|
+
for (let k = from; k < to && k < n; k++) if (out[k] !== '\n') out[k] = ' ';
|
|
85
|
+
};
|
|
86
|
+
// Literal interiors go through this one; comments always use `blank`.
|
|
87
|
+
const blankLiteral = keepLiterals ? () => {} : blank;
|
|
88
|
+
|
|
89
|
+
while (i < n) {
|
|
90
|
+
const c = text[i];
|
|
91
|
+
const next = text[i + 1];
|
|
92
|
+
|
|
93
|
+
// Line comment.
|
|
94
|
+
if (c === '/' && next === '/') {
|
|
95
|
+
let j = i;
|
|
96
|
+
while (j < n && text[j] !== '\n') j++;
|
|
97
|
+
blank(i, j);
|
|
98
|
+
i = j;
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
// Block comment — ONLY when it is actually closed.
|
|
102
|
+
//
|
|
103
|
+
// Named ceiling (BV-004), replacing an unbounded blank: an UNTERMINATED
|
|
104
|
+
// `/*` used to blank everything to EOF. In valid source that shape is a
|
|
105
|
+
// syntax error, so in practice it only ever arises from a lexer desync —
|
|
106
|
+
// a `/*` inside a regex literal the branch below failed to recognise
|
|
107
|
+
// (`return /\/*$/`). Blanking the rest of the file then hides every guard
|
|
108
|
+
// under it, which is a validator falling SILENT. The direction is therefore
|
|
109
|
+
// chosen fail-toward-REPORTING: an unclosed `/*` is not a comment at all,
|
|
110
|
+
// and the few bytes of a genuine one get scanned as code — at worst that
|
|
111
|
+
// produces a finding a human dismisses, never a silent pass.
|
|
112
|
+
// REVISIT TRIGGER: a tracked file that legitimately ends inside an
|
|
113
|
+
// unterminated block comment (i.e. the moment `node --check` accepts one).
|
|
114
|
+
if (c === '/' && next === '*') {
|
|
115
|
+
let j = i + 2;
|
|
116
|
+
while (j < n && !(text[j] === '*' && text[j + 1] === '/')) j++;
|
|
117
|
+
if (j < n) {
|
|
118
|
+
blank(i, j + 2);
|
|
119
|
+
i = j + 2;
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
// Unterminated — fall through and treat the `/` as ordinary source.
|
|
123
|
+
}
|
|
124
|
+
// Single/double-quoted string — keep the quotes, blank the interior.
|
|
125
|
+
if (c === "'" || c === '"') {
|
|
126
|
+
let j = i + 1;
|
|
127
|
+
while (j < n && text[j] !== c) {
|
|
128
|
+
if (text[j] === '\\') j++;
|
|
129
|
+
if (text[j] === '\n') break; // unterminated — bail at EOL
|
|
130
|
+
j++;
|
|
131
|
+
}
|
|
132
|
+
blankLiteral(i + 1, j);
|
|
133
|
+
i = Math.min(j + 1, n);
|
|
134
|
+
prev = c;
|
|
135
|
+
prevIdx = i - 1;
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
// Template literal — blank everything including `${…}` substitutions.
|
|
139
|
+
if (c === '`') {
|
|
140
|
+
let j = i + 1;
|
|
141
|
+
while (j < n && text[j] !== '`') {
|
|
142
|
+
if (text[j] === '\\') j++;
|
|
143
|
+
j++;
|
|
144
|
+
}
|
|
145
|
+
blankLiteral(i + 1, j);
|
|
146
|
+
i = Math.min(j + 1, n);
|
|
147
|
+
prev = c;
|
|
148
|
+
prevIdx = i - 1;
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
// Regex literal — only when the previous significant TOKEN cannot end an
|
|
152
|
+
// expression, AND a closing unescaped `/` exists on the SAME line. The
|
|
153
|
+
// same-line bound keeps a misread division from swallowing the file.
|
|
154
|
+
//
|
|
155
|
+
// The token is either the single char `prev`, or — when that char is a word
|
|
156
|
+
// character — the whole word ending at it, checked against
|
|
157
|
+
// REGEX_PRECEDING_KEYWORDS. Without that lookback `return /\/*$/` read as a
|
|
158
|
+
// division and its `/*` reached the block-comment branch above, which blanked
|
|
159
|
+
// the rest of the file: a validator silently passing (measured 2026-09-18).
|
|
160
|
+
//
|
|
161
|
+
// Named ceiling (BV-004): the lookback is exactly ONE token. A regex after
|
|
162
|
+
// any other non-expression-ending context — a `)` that closes `if (…)`, a
|
|
163
|
+
// statement label, `function` — is still read as division and its body
|
|
164
|
+
// scanned as code. That desync is now bounded on BOTH sides: a quote in it
|
|
165
|
+
// opens a string that bails at EOL, and a `/*` in it no longer blanks past
|
|
166
|
+
// the file (unterminated block comments are not comments — see above).
|
|
167
|
+
// REVISIT TRIGGER: a census finding that needs `)`-aware lookback, i.e. a
|
|
168
|
+
// regex used directly as the body of `if (…) /re/.test(x)`.
|
|
169
|
+
const startsRegex = c === '/' && (
|
|
170
|
+
prev === '' || '(,=:[!&|?{};+-*%~^<>'.includes(prev)
|
|
171
|
+
|| REGEX_PRECEDING_KEYWORDS.has(wordEndingAt(prevIdx))
|
|
172
|
+
);
|
|
173
|
+
if (startsRegex) {
|
|
174
|
+
let j = i + 1;
|
|
175
|
+
let inClass = false;
|
|
176
|
+
let closed = -1;
|
|
177
|
+
while (j < n && text[j] !== '\n') {
|
|
178
|
+
if (text[j] === '\\') { j += 2; continue; }
|
|
179
|
+
if (text[j] === '[') inClass = true;
|
|
180
|
+
else if (text[j] === ']') inClass = false;
|
|
181
|
+
else if (text[j] === '/' && !inClass) { closed = j; break; }
|
|
182
|
+
j++;
|
|
183
|
+
}
|
|
184
|
+
if (closed > i) {
|
|
185
|
+
blankLiteral(i + 1, closed);
|
|
186
|
+
i = closed + 1;
|
|
187
|
+
prev = '/';
|
|
188
|
+
prevIdx = closed;
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
if (!/\s/.test(c)) { prev = c; prevIdx = i; }
|
|
194
|
+
i++;
|
|
195
|
+
}
|
|
196
|
+
return out.join('');
|
|
197
|
+
}
|
|
@@ -11,13 +11,17 @@
|
|
|
11
11
|
* a CODE call site instead: `emitEvolveCompleted()` is invoked from
|
|
12
12
|
* `scripts/sweep-expired-learnings.mjs`'s `--prune` exit path (the ONE store
|
|
13
13
|
* write `/evolve analyze` already performs), and `recordDialecticRun()` is
|
|
14
|
-
* invoked from `scripts/dialectic-deriver.mjs::runDialecticDeriver()` for
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
* throw-based abort classes (`unknown-model`,
|
|
18
|
-
* `
|
|
19
|
-
*
|
|
20
|
-
*
|
|
14
|
+
* invoked from `scripts/dialectic-deriver.mjs::runDialecticDeriver()` for
|
|
15
|
+
* every outcome that pipeline can determine on its own: the `ok`/abort
|
|
16
|
+
* returns (`empty-input`, `budget-exceeded`, `would-empty-card`, dry-run
|
|
17
|
+
* success) AND the two throw-based abort classes (`unknown-model`,
|
|
18
|
+
* `subagent-crash`), the latter via that module's `recordThrownAbort()`
|
|
19
|
+
* helper which records at the throw point before the original error is
|
|
20
|
+
* rethrown. Only APPLY-mode success still calls `recordDialecticRun()` from
|
|
21
|
+
* `skills/evolve/SKILL.md` prose (Step 6.4 of
|
|
22
|
+
* `skills/evolve/references/evolve-dialectic-mode.md`) — the merge
|
|
23
|
+
* (`mergePeerCard()`) happens one layer up, outside this pure pipeline, so the
|
|
24
|
+
* real merge-stats deltas are not knowable inside `runDialecticDeriver()`.
|
|
21
25
|
*
|
|
22
26
|
* Both emitters are best-effort and try/catch-wrapped — same posture as
|
|
23
27
|
* `emitReconcileCompleted` in `scripts/lib/reconcile/engine.mjs` — because
|