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
|
@@ -22,10 +22,17 @@
|
|
|
22
22
|
* triageDecision(finding, candidates)
|
|
23
23
|
* → {action: 'create'|'update'|'ignore', target?, reason: string, confidence: number}
|
|
24
24
|
*
|
|
25
|
+
* buildIssueBody(finding, fp) → string
|
|
26
|
+
*
|
|
27
|
+
* neutralizeFingerprintSentinel(text) → string
|
|
28
|
+
* fingerprintSentinelLine(fp) → string
|
|
29
|
+
* The one shared sentinel writer + neutraliser, also used by
|
|
30
|
+
* ux-grill/reconcile.mjs (#1339 P6).
|
|
31
|
+
*
|
|
25
32
|
* ReconcileError
|
|
26
33
|
*
|
|
27
34
|
* Security:
|
|
28
|
-
* - #388 (SEC-IR-MED-1): sentinel injection hardening —
|
|
35
|
+
* - #388 (SEC-IR-MED-1): sentinel injection hardening — neutralizeFingerprintSentinel()
|
|
29
36
|
* strips **Fingerprint:** literals from free-text fields before they are
|
|
30
37
|
* embedded in the body (before the authoritative sentinel line is appended).
|
|
31
38
|
* - #389 (SEC-IR-LOW-1): maxBuffer set to 4 MB on every execFile call;
|
|
@@ -126,39 +133,64 @@ function validateFinding(finding) {
|
|
|
126
133
|
// ---------------------------------------------------------------------------
|
|
127
134
|
|
|
128
135
|
/**
|
|
129
|
-
*
|
|
130
|
-
*
|
|
131
|
-
*
|
|
132
|
-
*
|
|
136
|
+
* Neutralise every `**Fingerprint:**` literal in untrusted text by rewriting it
|
|
137
|
+
* to `__Fingerprint__`. The ONE copy of this neutralisation: this module's
|
|
138
|
+
* {@link buildIssueBody} and `ux-grill/reconcile.mjs` both route free text
|
|
139
|
+
* through it (#1339 P6), so a hardening added here reaches both producers.
|
|
133
140
|
*
|
|
134
|
-
* Context: glab parses issue bodies as Markdown. If
|
|
141
|
+
* Context: glab parses issue bodies as Markdown. If a free-text field contained
|
|
135
142
|
* `**Fingerprint:** <attacker-hash>`, a grep for the fingerprint sentinel in
|
|
136
143
|
* listExistingFindings() would match the spoofed value instead of the real one,
|
|
137
144
|
* bypassing dedup. Replacing the literal prevents the attack without altering
|
|
138
|
-
* the semantic meaning of the
|
|
145
|
+
* the semantic meaning of the text.
|
|
146
|
+
*
|
|
147
|
+
* @param {unknown} text - coerced with `String(text ?? '')`
|
|
148
|
+
* @returns {string}
|
|
149
|
+
*/
|
|
150
|
+
export function neutralizeFingerprintSentinel(text) {
|
|
151
|
+
// gi flag: case-insensitive match so **fingerprint:** and **FINGERPRINT:** variants
|
|
152
|
+
// are also neutralized. Sanitizer is intentionally broader than the case-sensitive
|
|
153
|
+
// extractor regex — this is the correct asymmetry (#388 SEC-IR-MED-1).
|
|
154
|
+
return String(text ?? '').replace(/\*\*Fingerprint:\*\*/gi, '__Fingerprint__');
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* The authoritative dedup sentinel line, in the exact shape
|
|
159
|
+
* {@link extractFingerprintFromBody} reads. The caller passes an
|
|
160
|
+
* already-trusted value; this function does not sanitise it.
|
|
161
|
+
*
|
|
162
|
+
* @param {string} fp - 16-char hex fingerprint
|
|
163
|
+
* @returns {string}
|
|
164
|
+
*/
|
|
165
|
+
export function fingerprintSentinelLine(fp) {
|
|
166
|
+
return `**Fingerprint:** \`${fp}\``;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Sanitize free-text recommendation fields before embedding in the issue body
|
|
171
|
+
* (#388 SEC-IR-MED-1) — {@link neutralizeFingerprintSentinel} for a non-empty
|
|
172
|
+
* string, anything else returned unchanged (a `null` description is dropped by
|
|
173
|
+
* buildIssueBody's line filter rather than rendered as an empty line).
|
|
139
174
|
*
|
|
140
175
|
* @param {string|undefined|null} text
|
|
141
176
|
* @returns {string|undefined|null}
|
|
142
177
|
*/
|
|
143
178
|
function sanitizeRecommendation(text) {
|
|
144
179
|
if (!text || typeof text !== 'string') return text;
|
|
145
|
-
|
|
146
|
-
// are also neutralized. Sanitizer is intentionally broader than the case-sensitive
|
|
147
|
-
// extractor regex — this is the correct asymmetry (#388 SEC-IR-MED-1).
|
|
148
|
-
return text.replace(/\*\*Fingerprint:\*\*/gi, '__Fingerprint__');
|
|
180
|
+
return neutralizeFingerprintSentinel(text);
|
|
149
181
|
}
|
|
150
182
|
|
|
151
183
|
/**
|
|
152
184
|
* Render untrusted text as the content of ONE inline Markdown code span:
|
|
153
|
-
* sentinel literals neutralised
|
|
154
|
-
*
|
|
155
|
-
*
|
|
185
|
+
* sentinel literals neutralised, line breaks and NUL folded to a space,
|
|
186
|
+
* backticks turned into `'` — so a page-controlled locator can neither close
|
|
187
|
+
* its code span nor start a new body line.
|
|
156
188
|
*
|
|
157
189
|
* @param {unknown} text
|
|
158
190
|
* @returns {string}
|
|
159
191
|
*/
|
|
160
192
|
function codeSpanContent(text) {
|
|
161
|
-
return
|
|
193
|
+
return neutralizeFingerprintSentinel(text)
|
|
162
194
|
.replace(/[\r\n\0]+/g, ' ')
|
|
163
195
|
.replace(/`/g, "'");
|
|
164
196
|
}
|
|
@@ -218,7 +250,7 @@ export function buildIssueBody(finding, fp) {
|
|
|
218
250
|
'',
|
|
219
251
|
safeRecommendation ? `**Recommendation:** ${safeRecommendation}` : null,
|
|
220
252
|
'',
|
|
221
|
-
|
|
253
|
+
fingerprintSentinelLine(fp),
|
|
222
254
|
`**Severity:** ${finding.severity}`,
|
|
223
255
|
`**Check:** ${finding.checkId}`,
|
|
224
256
|
`**Locator:** \`${codeSpanContent(finding.locator)}\``,
|
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
* scripts/lib/tmux-layout/telemetry-stats.mjs
|
|
3
3
|
*
|
|
4
4
|
* Aggregator stub for /tmux-layout telemetry promotion gate (#563).
|
|
5
|
-
* Reads
|
|
5
|
+
* Reads the events ledger — active file AND every rotated archive (#1407) —
|
|
6
|
+
* and computes:
|
|
6
7
|
* - invocation count (total + per layout)
|
|
7
8
|
* - completion rate (completed / invoked) — promotion gate threshold 80%
|
|
8
9
|
* - top-K degradation reasons
|
|
@@ -13,26 +14,68 @@
|
|
|
13
14
|
* - CLI: `node scripts/lib/tmux-layout/telemetry-stats.mjs` (emits JSON to stdout)
|
|
14
15
|
*/
|
|
15
16
|
|
|
16
|
-
import
|
|
17
|
+
import path from 'node:path';
|
|
18
|
+
import { readEventsWithRotations } from '../events.mjs';
|
|
17
19
|
import { isMainModule } from '../is-main-module.mjs';
|
|
18
20
|
|
|
19
21
|
const EVENTS_PATH = '.orchestrator/metrics/events.jsonl';
|
|
20
22
|
|
|
23
|
+
/** Every event this module reports on carries this prefix. */
|
|
24
|
+
const TMUX_EVENT_PREFIX = 'tmux-layout.';
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Read the tmux-layout events ACROSS rotation boundaries, together with the
|
|
28
|
+
* honesty verdict of that read.
|
|
29
|
+
*
|
|
30
|
+
* WHY THE ENVELOPE (#1407 acceptance criterion 3): the stats below are an
|
|
31
|
+
* ALL-TIME rate. Reading only the active file silently redefines "all time" as
|
|
32
|
+
* "since the last rotation", and a genuinely missing archive would then look
|
|
33
|
+
* identical to a quiet week. `complete === false` says which of the two it is;
|
|
34
|
+
* the CLI prints it and never computes over a partial set in silence.
|
|
35
|
+
*
|
|
36
|
+
* THREE states (#1423): `complete === null` is a THIRD reading — no source
|
|
37
|
+
* existed at all, so `invocations: 0` is UNMEASURED, not a measured zero, and
|
|
38
|
+
* `meetsPromotionGate: false` below rests on nothing. The CLI names that case
|
|
39
|
+
* separately; folding it into `false` would report a gap nobody found, folding
|
|
40
|
+
* it into `true` would report a measurement nobody took.
|
|
41
|
+
*
|
|
42
|
+
* CEILING (BV-004): `readEventsWithRotations` loads the active file and every
|
|
43
|
+
* archive fully into memory — up to ~60 MB transient at the default
|
|
44
|
+
* `max-size-mb: 10` / `max-backups: 5`. Acceptable here because this is a COLD
|
|
45
|
+
* ops path: a hand-run CLI / promotion-gate check, never a hook and never on
|
|
46
|
+
* the session path. Revisit if this module gains a hot-path caller or if
|
|
47
|
+
* `max-size-mb` is raised past ~100.
|
|
48
|
+
*
|
|
49
|
+
* @param {string} [eventsPath=.orchestrator/metrics/events.jsonl]
|
|
50
|
+
* @returns {{events: Array<object>, complete: boolean|null, gaps: Array<object>,
|
|
51
|
+
* notices: Array<object>}}
|
|
52
|
+
*/
|
|
53
|
+
export function readTmuxEventsEnvelope(eventsPath = EVENTS_PATH) {
|
|
54
|
+
const { events, gaps, complete, notices } = readEventsWithRotations(undefined, {
|
|
55
|
+
filePath: eventsPath,
|
|
56
|
+
});
|
|
57
|
+
return {
|
|
58
|
+
events: events.filter(
|
|
59
|
+
(rec) => rec && typeof rec.event === 'string' && rec.event.startsWith(TMUX_EVENT_PREFIX),
|
|
60
|
+
),
|
|
61
|
+
complete,
|
|
62
|
+
gaps,
|
|
63
|
+
notices: notices ?? [],
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
21
67
|
/**
|
|
22
|
-
* Read events
|
|
68
|
+
* Read the events ledger and return tmux-layout-related events.
|
|
69
|
+
*
|
|
70
|
+
* Thin array-returning view of {@link readTmuxEventsEnvelope} — the shape every
|
|
71
|
+
* existing caller expects. Use the envelope when the completeness of the read
|
|
72
|
+
* matters to the answer.
|
|
23
73
|
*
|
|
24
74
|
* @param {string} [eventsPath=.orchestrator/metrics/events.jsonl]
|
|
25
75
|
* @returns {Array<object>} parsed event records (filtered to tmux-layout.* events)
|
|
26
76
|
*/
|
|
27
77
|
export function readTmuxEvents(eventsPath = EVENTS_PATH) {
|
|
28
|
-
|
|
29
|
-
const raw = readFileSync(eventsPath, 'utf-8');
|
|
30
|
-
const lines = raw.split('\n').filter((l) => l.trim().length > 0);
|
|
31
|
-
return lines
|
|
32
|
-
.map((l) => {
|
|
33
|
-
try { return JSON.parse(l); } catch { return null; }
|
|
34
|
-
})
|
|
35
|
-
.filter((rec) => rec && typeof rec.event === 'string' && rec.event.startsWith('tmux-layout.'));
|
|
78
|
+
return readTmuxEventsEnvelope(eventsPath).events;
|
|
36
79
|
}
|
|
37
80
|
|
|
38
81
|
/**
|
|
@@ -95,7 +138,23 @@ export function computeStats(events) {
|
|
|
95
138
|
|
|
96
139
|
// CLI entry-point — emit stats as JSON to stdout when run directly
|
|
97
140
|
if (isMainModule(import.meta.url)) {
|
|
98
|
-
const events =
|
|
141
|
+
const { events, complete, gaps } = readTmuxEventsEnvelope();
|
|
99
142
|
const stats = computeStats(events);
|
|
100
|
-
|
|
143
|
+
if (complete === null) {
|
|
144
|
+
// NOT MEASURED — a different sentence from "incomplete" (#1423): no active
|
|
145
|
+
// file and no archive means the zeros below were never observed.
|
|
146
|
+
console.error(
|
|
147
|
+
'WARN: no events source was readable (no active file, no archive) — the stats below are UNMEASURED, not zero',
|
|
148
|
+
);
|
|
149
|
+
} else if (!complete) {
|
|
150
|
+
// A gap, never an empty window: diagnostics on stderr (cli-design.md),
|
|
151
|
+
// the machine-readable verdict in the JSON below.
|
|
152
|
+
const detail = gaps
|
|
153
|
+
.map((g) => `${g.kind}:${path.basename(String(g.archived_as ?? 'unknown'))}`)
|
|
154
|
+
.join(', ');
|
|
155
|
+
console.error(
|
|
156
|
+
`WARN: events ledger incomplete — the all-time rate below is computed over a PARTIAL set (${gaps.length} gap(s): ${detail})`,
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
console.log(JSON.stringify({ ...stats, ledgerComplete: complete, ledgerGaps: gaps }, null, 2));
|
|
101
160
|
}
|
|
@@ -102,11 +102,31 @@ function normaliseScalar(text) {
|
|
|
102
102
|
* than inventing a fifth dialect. Because the demotion is the surprising half,
|
|
103
103
|
* each one emits exactly one stderr WARN naming the file.
|
|
104
104
|
*
|
|
105
|
+
* The same normalisation governs the SIBLING boolean marker
|
|
106
|
+
* `disable-model-invocation` (the Cursor and Codex adapters read it with this
|
|
107
|
+
* predicate). The WARN therefore names the key it was asked about rather than a
|
|
108
|
+
* hard-coded `user-invocable`: measured 2026-09-18, a `disable-model-invocation:
|
|
109
|
+
* yes` demotion reported `user-invocable: "yes"`, sending the operator to a
|
|
110
|
+
* line that does not exist in that file.
|
|
111
|
+
*
|
|
112
|
+
* The NOUN follows the key too (#1388 P10). Naming the key while hard-coding
|
|
113
|
+
* "is NOT a slash-command marker" told a `disable-model-invocation: yes` author
|
|
114
|
+
* that his value failed to be something it never was — a correct key with a
|
|
115
|
+
* wrong subject reads as a broken diagnostic. The default phrase is unchanged
|
|
116
|
+
* for `user-invocable`, so the four existing call sites see identical text.
|
|
117
|
+
*
|
|
105
118
|
* @param {unknown} value the raw frontmatter value
|
|
106
119
|
* @param {string} [file] path named in the WARN when a truthy-looking value is demoted
|
|
120
|
+
* @param {string} [key] frontmatter key named in the WARN (default `user-invocable`)
|
|
107
121
|
* @returns {boolean}
|
|
108
122
|
*/
|
|
109
|
-
|
|
123
|
+
const MARKER_NOUN = Object.freeze({
|
|
124
|
+
'user-invocable': 'a slash-command marker',
|
|
125
|
+
'disable-model-invocation': 'a model-invocation opt-out marker',
|
|
126
|
+
});
|
|
127
|
+
const DEFAULT_MARKER_NOUN = 'a boolean marker';
|
|
128
|
+
|
|
129
|
+
export function isUserInvocableValue(value, file, key = 'user-invocable') {
|
|
110
130
|
if (value === true) return true;
|
|
111
131
|
if (typeof value !== 'string') return false;
|
|
112
132
|
const token = normaliseScalar(value);
|
|
@@ -114,8 +134,8 @@ export function isUserInvocableValue(value, file) {
|
|
|
114
134
|
if (TRUTHY_LOOKALIKE.test(token)) {
|
|
115
135
|
const where = file ? `${file}: ` : '';
|
|
116
136
|
process.stderr.write(
|
|
117
|
-
`WARN ${where}
|
|
118
|
-
+
|
|
137
|
+
`WARN ${where}${key}: ${JSON.stringify(value)} is NOT ${MARKER_NOUN[key] ?? DEFAULT_MARKER_NOUN} — `
|
|
138
|
+
+ `only \`true\` is (YAML 1.2 reads yes/on/1 as strings). Write \`${key}: true\`.\n`,
|
|
119
139
|
);
|
|
120
140
|
}
|
|
121
141
|
return false;
|
|
@@ -37,12 +37,16 @@
|
|
|
37
37
|
* mistaken import away from filing issues nobody asked for.
|
|
38
38
|
*
|
|
39
39
|
* Exports:
|
|
40
|
-
* UX_GRILL_LABELS, buildIssueTitle,
|
|
40
|
+
* UX_GRILL_LABELS, buildIssueTitle, buildUxGrillIssueBody, reconcileFindings
|
|
41
|
+
* buildIssueBody — @deprecated alias of buildUxGrillIssueBody (5.2.0 name),
|
|
42
|
+
* removed in 6.0.0
|
|
41
43
|
*/
|
|
42
44
|
|
|
43
45
|
import {
|
|
44
46
|
createFinding as defaultCreateFinding,
|
|
47
|
+
fingerprintSentinelLine,
|
|
45
48
|
listExistingFindings as defaultListExisting,
|
|
49
|
+
neutralizeFingerprintSentinel,
|
|
46
50
|
triageDecision,
|
|
47
51
|
updateFinding as defaultUpdateFinding,
|
|
48
52
|
} from '../test-runner/issue-reconcile.mjs';
|
|
@@ -69,31 +73,22 @@ const TITLE_MAX_LENGTH = 100;
|
|
|
69
73
|
const AUTO_SEVERITIES = Object.freeze(['critical', 'high']);
|
|
70
74
|
|
|
71
75
|
/**
|
|
72
|
-
* Neutralise
|
|
76
|
+
* Neutralise fingerprint-sentinel literals (#388 SEC-IR-MED-1) and strip the
|
|
77
|
+
* characters `createFinding` rejects at the argv boundary, so a multi-line page
|
|
78
|
+
* title in a finding message cannot turn into a VALIDATION failure for the whole
|
|
79
|
+
* reconcile pass.
|
|
73
80
|
*
|
|
74
|
-
* `issue-reconcile.mjs`
|
|
75
|
-
*
|
|
76
|
-
*
|
|
77
|
-
*
|
|
81
|
+
* The neutralisation is the SHARED one from `issue-reconcile.mjs` (#1339 P6): a
|
|
82
|
+
* body built HERE and handed to `createFinding` is not sanitised by anything
|
|
83
|
+
* else, so a finding `message` echoing page text could otherwise forge the
|
|
84
|
+
* authoritative `**Fingerprint:**` line and make the next run's
|
|
78
85
|
* `triageDecision` dedup against the wrong issue.
|
|
79
86
|
*
|
|
80
87
|
* @param {unknown} text
|
|
81
88
|
* @returns {string}
|
|
82
89
|
*/
|
|
83
|
-
function sanitizeSentinel(text) {
|
|
84
|
-
return String(text ?? '').replace(/\*\*Fingerprint:\*\*/gi, '__Fingerprint__');
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
/**
|
|
88
|
-
* Strip the characters `createFinding` rejects at the argv boundary, so a
|
|
89
|
-
* multi-line page title in a finding message cannot turn into a VALIDATION
|
|
90
|
-
* failure for the whole reconcile pass.
|
|
91
|
-
*
|
|
92
|
-
* @param {unknown} text
|
|
93
|
-
* @returns {string}
|
|
94
|
-
*/
|
|
95
90
|
function oneLine(text) {
|
|
96
|
-
return
|
|
91
|
+
return neutralizeFingerprintSentinel(text).replace(/[\n\r\0]+/g, ' ').trim();
|
|
97
92
|
}
|
|
98
93
|
|
|
99
94
|
/**
|
|
@@ -144,7 +139,7 @@ export function buildIssueTitle(finding) {
|
|
|
144
139
|
* @param {string} [context.rubricHash]
|
|
145
140
|
* @returns {string}
|
|
146
141
|
*/
|
|
147
|
-
export function
|
|
142
|
+
export function buildUxGrillIssueBody(finding, { runId, rubricHash } = {}) {
|
|
148
143
|
const evidence = finding?.evidence ?? {};
|
|
149
144
|
const evidenceLines = Object.entries(evidence)
|
|
150
145
|
.filter(([, value]) => typeof value === 'string' || Array.isArray(value))
|
|
@@ -153,7 +148,7 @@ export function buildIssueBody(finding, { runId, rubricHash } = {}) {
|
|
|
153
148
|
return [
|
|
154
149
|
oneLine(finding?.message) || `${oneLine(finding?.checkId)} at ${oneLine(finding?.locator)}`,
|
|
155
150
|
'',
|
|
156
|
-
|
|
151
|
+
fingerprintSentinelLine(oneLine(finding?.fingerprint)),
|
|
157
152
|
`**Severity:** ${oneLine(finding?.severity)}`,
|
|
158
153
|
`**Check:** ${oneLine(finding?.checkId)}`,
|
|
159
154
|
`**Locator:** \`${oneLine(finding?.locator)}\``,
|
|
@@ -168,6 +163,37 @@ export function buildIssueBody(finding, { runId, rubricHash } = {}) {
|
|
|
168
163
|
.join('\n');
|
|
169
164
|
}
|
|
170
165
|
|
|
166
|
+
/** One stderr line per process for the deprecated alias below, not one per call. */
|
|
167
|
+
let buildIssueBodyDeprecationWarned = false;
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Deprecated alias of {@link buildUxGrillIssueBody}, shipped under this name in
|
|
171
|
+
* 5.2.0.
|
|
172
|
+
*
|
|
173
|
+
* @deprecated since 5.3.0 — use buildUxGrillIssueBody; removed in 6.0.0.
|
|
174
|
+
* Kept one minor cycle (`.claude/rules/development.md` § Package Lifecycle &
|
|
175
|
+
* Versioning) because `package.json` carries no `exports` map: every packed
|
|
176
|
+
* `scripts/lib/` export is a public deep-import, and a consumer calling this
|
|
177
|
+
* name would otherwise get a runtime TypeError, not a compile error. It
|
|
178
|
+
* delegates to the LOCAL ux-grill body builder — never to the unrelated
|
|
179
|
+
* `buildIssueBody(finding, fp)` of `test-runner/issue-reconcile.mjs`, whose
|
|
180
|
+
* finding shape and second argument differ.
|
|
181
|
+
*
|
|
182
|
+
* @param {object} finding - a `makeFinding` record
|
|
183
|
+
* @param {object} [context] - same as {@link buildUxGrillIssueBody}
|
|
184
|
+
* @returns {string}
|
|
185
|
+
*/
|
|
186
|
+
export function buildIssueBody(finding, context) {
|
|
187
|
+
if (!buildIssueBodyDeprecationWarned) {
|
|
188
|
+
buildIssueBodyDeprecationWarned = true;
|
|
189
|
+
console.warn(
|
|
190
|
+
'[deprecated] buildIssueBody from scripts/lib/ux-grill/reconcile.mjs is deprecated since 5.3.0 ' +
|
|
191
|
+
'and will be removed in 6.0.0 — use buildUxGrillIssueBody instead.',
|
|
192
|
+
);
|
|
193
|
+
}
|
|
194
|
+
return buildUxGrillIssueBody(finding, context);
|
|
195
|
+
}
|
|
196
|
+
|
|
171
197
|
/**
|
|
172
198
|
* Collect every string in a finding's `evidence` that looks like an absolute
|
|
173
199
|
* host path (POSIX `/…` or Windows `C:\…`).
|
|
@@ -296,7 +322,7 @@ export async function reconcileFindings({
|
|
|
296
322
|
}
|
|
297
323
|
|
|
298
324
|
const title = buildIssueTitle(finding);
|
|
299
|
-
const body =
|
|
325
|
+
const body = buildUxGrillIssueBody(finding, { runId, rubricHash });
|
|
300
326
|
const decision = triageDecision({ fingerprint: finding?.fingerprint, title }, candidates);
|
|
301
327
|
|
|
302
328
|
if (decision.action === 'ignore') {
|
|
@@ -11,16 +11,16 @@
|
|
|
11
11
|
* inode, or byte-identical).
|
|
12
12
|
* B. root `plugin.json` — the agent-plugins.org 1.0.0 manifest; its `version`
|
|
13
13
|
* must equal `package.json`'s and the two existing plugin manifests'.
|
|
14
|
-
* C. `.agents/skills/<name>/SKILL.md` — one spec-legal mirror per
|
|
15
|
-
*
|
|
14
|
+
* C. `.agents/skills/<name>/SKILL.md` — one spec-legal mirror per skill or
|
|
15
|
+
* command, no orphans, agentskills.io field list only, description ≤ 1024.
|
|
16
16
|
*
|
|
17
17
|
* INDEPENDENCE (load-bearing). This checker deliberately imports NOTHING from
|
|
18
18
|
* `scripts/generate-agents-skills.mjs` and re-derives the spec field list, the
|
|
19
19
|
* 1024 cap and the alias rule from the spec, not from the generator. A
|
|
20
20
|
* generator-vs-generator check only proves the generator is self-consistent —
|
|
21
21
|
* that is how the Cursor `argument-hint` defect survived. The oracles here are
|
|
22
|
-
* the repository's own filesystem (`skills/`, `CLAUDE.md`,
|
|
23
|
-
* the published field list.
|
|
22
|
+
* the repository's own filesystem (`skills/`, `commands/`, `CLAUDE.md`,
|
|
23
|
+
* `package.json`) and the published field list.
|
|
24
24
|
*
|
|
25
25
|
* Usage: node scripts/lib/validate/check-agents-skills.mjs <plugin-root>
|
|
26
26
|
* Exit codes: 0 — all checks passed · 1 — one or more failures.
|
|
@@ -195,6 +195,7 @@ console.log('');
|
|
|
195
195
|
console.log('--- Check C: .agents/skills/ portable mirror ---');
|
|
196
196
|
{
|
|
197
197
|
const skillsDir = join(pluginRoot, 'skills');
|
|
198
|
+
const commandsDir = join(pluginRoot, 'commands');
|
|
198
199
|
const mirrorDir = join(pluginRoot, '.agents', 'skills');
|
|
199
200
|
|
|
200
201
|
const sourceSkills = existsSync(skillsDir) && statSync(skillsDir).isDirectory()
|
|
@@ -204,12 +205,21 @@ console.log('--- Check C: .agents/skills/ portable mirror ---');
|
|
|
204
205
|
.filter((n) => existsSync(join(skillsDir, n, 'SKILL.md')))
|
|
205
206
|
.sort()
|
|
206
207
|
: [];
|
|
208
|
+
const sources = new Map(sourceSkills.map((name) => [name, `skills/${name}/SKILL.md`]));
|
|
209
|
+
if (existsSync(commandsDir) && statSync(commandsDir).isDirectory()) {
|
|
210
|
+
for (const entry of readdirSync(commandsDir, { withFileTypes: true })) {
|
|
211
|
+
if (!entry.isFile() || !entry.name.endsWith('.md') || /^[._]/.test(entry.name)) continue;
|
|
212
|
+
// Commands own public names when an internal skill has the same name.
|
|
213
|
+
sources.set(entry.name.slice(0, -3), `commands/${entry.name}`);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
const sourceNames = [...sources.keys()].sort();
|
|
207
217
|
|
|
208
218
|
if (!existsSync(mirrorDir)) {
|
|
209
|
-
if (
|
|
210
|
-
pass('no
|
|
219
|
+
if (sourceNames.length === 0) {
|
|
220
|
+
pass('no skill or command sources and no .agents/skills/ — nothing to mirror');
|
|
211
221
|
} else {
|
|
212
|
-
fail(`.agents/skills/ is missing but ${
|
|
222
|
+
fail(`.agents/skills/ is missing but ${sourceNames.length} source entry(s) exist `
|
|
213
223
|
+ '(remedy: node scripts/generate-agents-skills.mjs)');
|
|
214
224
|
}
|
|
215
225
|
} else {
|
|
@@ -218,12 +228,12 @@ console.log('--- Check C: .agents/skills/ portable mirror ---');
|
|
|
218
228
|
.map((e) => e.name)
|
|
219
229
|
.sort();
|
|
220
230
|
|
|
221
|
-
const missing =
|
|
222
|
-
const orphans = mirrored.filter((n) => !
|
|
223
|
-
if (missing.length === 0) pass(`every source
|
|
224
|
-
else fail(`${missing.length} source
|
|
231
|
+
const missing = sourceNames.filter((n) => !mirrored.includes(n));
|
|
232
|
+
const orphans = mirrored.filter((n) => !sources.has(n));
|
|
233
|
+
if (missing.length === 0) pass(`every source entry has a mirror (${sourceNames.length})`);
|
|
234
|
+
else fail(`${missing.length} source entry(s) have no mirror: ${missing.join(', ')}`);
|
|
225
235
|
if (orphans.length === 0) pass('no orphan mirrors');
|
|
226
|
-
else fail(`${orphans.length} orphan mirror(s) with no source
|
|
236
|
+
else fail(`${orphans.length} orphan mirror(s) with no skill or command source: ${orphans.join(', ')}`);
|
|
227
237
|
|
|
228
238
|
let fmFail = 0;
|
|
229
239
|
let keyFail = 0;
|
|
@@ -303,8 +313,9 @@ console.log('--- Check C: .agents/skills/ portable mirror ---');
|
|
|
303
313
|
}
|
|
304
314
|
}
|
|
305
315
|
|
|
306
|
-
|
|
307
|
-
|
|
316
|
+
const canonical = sources.get(name);
|
|
317
|
+
if (canonical && !body.includes(canonical)) {
|
|
318
|
+
fail(`.agents/skills/${name}/SKILL.md body does not cite the canonical ${canonical} `
|
|
308
319
|
+ '— the mirror is a pointer, and a pointer with no target is dead weight');
|
|
309
320
|
pointerFail += 1;
|
|
310
321
|
}
|
|
@@ -314,7 +325,7 @@ console.log('--- Check C: .agents/skills/ portable mirror ---');
|
|
|
314
325
|
if (keyFail === 0 && mirrored.length > 0) pass('all mirrors use only agentskills.io spec fields');
|
|
315
326
|
if (descFail === 0 && mirrored.length > 0) pass(`all mirror descriptions are non-empty and ≤ ${DESCRIPTION_MAX} chars`);
|
|
316
327
|
if (sizeFail === 0 && mirrored.length > 0) pass(`all mirrors ≤ ${MIRROR_MAX_BYTES} bytes (total ${totalBytes} bytes)`);
|
|
317
|
-
if (pointerFail === 0 && mirrored.length > 0) pass('all mirrors cite their canonical
|
|
328
|
+
if (pointerFail === 0 && mirrored.length > 0) pass('all mirrors cite their canonical source');
|
|
318
329
|
}
|
|
319
330
|
}
|
|
320
331
|
|
|
@@ -177,6 +177,7 @@ function runCli(pluginRoot) {
|
|
|
177
177
|
} else {
|
|
178
178
|
const detail = ((result.stdout ?? '') + (result.stderr ?? '')).trim();
|
|
179
179
|
fail(`Cursor adapter wrappers are stale${detail ? `: ${detail}` : ''}`);
|
|
180
|
+
console.log(' Remedy: node scripts/generate-cursor-adapter.mjs');
|
|
180
181
|
}
|
|
181
182
|
}
|
|
182
183
|
|
|
@@ -76,6 +76,7 @@ import { readFileSync } from 'node:fs';
|
|
|
76
76
|
import path from 'node:path';
|
|
77
77
|
|
|
78
78
|
import { isMainModule } from '../is-main-module.mjs';
|
|
79
|
+
import { maskSource } from '../js-mask.mjs';
|
|
79
80
|
|
|
80
81
|
/**
|
|
81
82
|
* Files whose `process.argv[1]` comparison the oracle flags but which are NOT
|
|
@@ -102,50 +103,20 @@ const SELF_EXEMPT = 'scripts/lib/is-main-module.mjs';
|
|
|
102
103
|
/**
|
|
103
104
|
* Remove comments while preserving character offsets (so line numbers survive).
|
|
104
105
|
*
|
|
106
|
+
* A thin wrapper over the shared lexer `maskSource` in `keepLiterals` mode:
|
|
107
|
+
* string, template and regex literals are skipped INTACT. That is load-bearing
|
|
108
|
+
* twice over — the V1 idiom \`file://${process.argv[1]}\` carries a `//` that a
|
|
109
|
+
* naive scanner reads as a line comment, and a regex literal must be recognised
|
|
110
|
+
* as one (#1383: a `/` the lexer read as a division let the regex body's `/*`
|
|
111
|
+
* open a block comment that blanked the guard below it — a fail-open miss, for
|
|
112
|
+
* a quote-bearing regex, a `/[/*]/` character class, and a regex after a keyword
|
|
113
|
+
* alike).
|
|
114
|
+
*
|
|
105
115
|
* @param {string} src module source
|
|
106
116
|
* @returns {string} same length, comment bytes replaced by spaces
|
|
107
117
|
*/
|
|
108
118
|
export function stripComments(src) {
|
|
109
|
-
|
|
110
|
-
let i = 0;
|
|
111
|
-
let mode = 'code';
|
|
112
|
-
while (i < src.length) {
|
|
113
|
-
const two = src.slice(i, i + 2);
|
|
114
|
-
if (mode === 'code') {
|
|
115
|
-
if (two === '//') mode = 'line';
|
|
116
|
-
else if (two === '/*') mode = 'block';
|
|
117
|
-
else if (src[i] === '`' || src[i] === "'" || src[i] === '"') {
|
|
118
|
-
// Skip OVER the literal to its closing delimiter, leaving the contents
|
|
119
|
-
// intact. This is load-bearing, not hygiene: the V1 idiom is
|
|
120
|
-
// \`file://${process.argv[1]}\` and the `//` in `file://` reads as a line
|
|
121
|
-
// comment to a naive scanner, which blanks the rest of the line and
|
|
122
|
-
// makes the most common variant of the defect class invisible.
|
|
123
|
-
const quote = src[i];
|
|
124
|
-
i++;
|
|
125
|
-
while (i < src.length && src[i] !== quote) {
|
|
126
|
-
if (src[i] === '\\') i++;
|
|
127
|
-
i++;
|
|
128
|
-
}
|
|
129
|
-
i++;
|
|
130
|
-
continue;
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
if (mode === 'line') {
|
|
134
|
-
if (src[i] === '\n') mode = 'code';
|
|
135
|
-
else out[i] = ' ';
|
|
136
|
-
} else if (mode === 'block') {
|
|
137
|
-
if (src[i] !== '\n') out[i] = ' ';
|
|
138
|
-
if (two === '*/') {
|
|
139
|
-
out[i] = ' ';
|
|
140
|
-
out[i + 1] = ' ';
|
|
141
|
-
i += 2;
|
|
142
|
-
mode = 'code';
|
|
143
|
-
continue;
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
i++;
|
|
147
|
-
}
|
|
148
|
-
return out.join('');
|
|
119
|
+
return maskSource(src, { keepLiterals: true });
|
|
149
120
|
}
|
|
150
121
|
|
|
151
122
|
/**
|
|
@@ -153,16 +124,8 @@ export function stripComments(src) {
|
|
|
153
124
|
* Scanned rather than matched in one shot so the RECEIVER can be judged from the
|
|
154
125
|
* text preceding each hit — a `.endsWith()` on anything other than
|
|
155
126
|
* `process.argv[1]` is not this defect class.
|
|
156
|
-
*
|
|
157
|
-
* The quote characters are spelled `\x27` / `\x22` / `\x60` rather than
|
|
158
|
-
* literally, and that is load-bearing for THIS file specifically: `stripComments`
|
|
159
|
-
* below has no notion of a regex literal, so a bare `'` inside one reads as the
|
|
160
|
-
* start of a string and desynchronises the scanner for the rest of the module —
|
|
161
|
-
* measured 2026-09-17, a literal-quote version of this regex made the census
|
|
162
|
-
* report a line-comment 40 lines further down as a finding. Same rule applies to
|
|
163
|
-
* every regex added to this module.
|
|
164
127
|
*/
|
|
165
|
-
const ENDS_WITH_LITERAL = /endsWith\(\s*([
|
|
128
|
+
const ENDS_WITH_LITERAL = /endsWith\(\s*(['"`])([^'"`]*)\1\s*\)/g;
|
|
166
129
|
|
|
167
130
|
/**
|
|
168
131
|
* The receiver chain, whitespace-stripped, that makes an `.endsWith()` call a
|
|
@@ -170,7 +133,7 @@ const ENDS_WITH_LITERAL = /endsWith\(\s*([\x27\x22\x60])([^\x27\x22\x60]*)\1\s*\
|
|
|
170
133
|
* wrapper (`String(...)`, `path.basename(...)`) still matches; anchored at the
|
|
171
134
|
* END so only the call immediately downstream of `process.argv[1]` counts.
|
|
172
135
|
*/
|
|
173
|
-
const ARGV_RECEIVER_TAIL = /process\.argv\[1\](
|
|
136
|
+
const ARGV_RECEIVER_TAIL = /process\.argv\[1\](?:\|\|''|\|\|"")?\)*\??\.$/;
|
|
174
137
|
|
|
175
138
|
/**
|
|
176
139
|
* Findings for one module body.
|