session-orchestrator 4.1.0 → 5.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.agents/skills/session-plan/SKILL.md +1 -1
- package/.agents/skills/session-start/SKILL.md +1 -1
- package/.agents/skills/ux-grill/SKILL.md +22 -0
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +3 -2
- package/.codex-plugin/plugin.json +1 -1
- package/.codex-plugin/skills/session-plan/SKILL.md +1 -1
- package/.codex-plugin/skills/session-start/SKILL.md +1 -1
- package/.codex-plugin/skills/ux-grill/SKILL.md +21 -0
- package/.codex-plugin/skills/ux-grill/agents/openai.yaml +5 -0
- package/.cursor/commands/ux-grill.md +14 -0
- package/.cursor/skills/session-plan/SKILL.md +1 -1
- package/.cursor/skills/session-start/SKILL.md +1 -1
- package/.cursor/skills/ux-grill/SKILL.md +13 -0
- package/.cursor-plugin/plugin.json +1 -1
- package/AGENTS.md +2 -1
- package/CHANGELOG.md +128 -1
- package/README.md +98 -86
- package/agents/dialectic-deriver.md +11 -0
- package/agents/ux-evaluator.md +1 -1
- package/commands/close.md +3 -3
- package/commands/go.md +2 -0
- package/commands/memory-cleanup.md +4 -3
- package/commands/persona-panel.md +1 -1
- package/commands/session.md +3 -2
- package/commands/ux-grill.md +51 -0
- package/docs/README.md +4 -4
- package/docs/USER-GUIDE.md +117 -50
- package/docs/agent-authoring.md +2 -2
- package/docs/baseline.md +55 -1
- package/docs/ci-setup.md +1 -1
- package/docs/codex-setup.md +9 -0
- package/docs/components.md +9 -9
- package/docs/cursor-setup.md +1 -0
- package/docs/events-schema.md +13 -6
- package/docs/github-mirror-protection.md +61 -20
- package/docs/instruction-delivery.md +1 -1
- package/docs/memory-proposal-flow.md +3 -3
- package/docs/migration-v4.md +2 -2
- package/docs/migration-v5.md +62 -0
- package/docs/owner-config-schema.md +74 -90
- package/docs/persona-panel.md +4 -4
- package/docs/pi-setup.md +1 -0
- package/docs/rule-authoring.md +13 -6
- package/docs/scope-collision-guard.md +16 -0
- package/docs/session-config-reference.md +55 -22
- package/docs/session-config-template.md +9 -5
- package/docs/vault-docs-architecture.md +4 -2
- package/hooks/_lib/hook-import-set.json +70 -3
- package/hooks/_lib/lock-bootstrap.mjs +84 -1
- package/hooks/_lib/vcs-create-matcher.mjs +401 -16
- package/hooks/enforce-scope.mjs +201 -0
- package/hooks/hooks-codex.json +1 -1
- package/hooks/hooks-cursor.json +5 -0
- package/hooks/hooks.json +7 -2
- package/hooks/on-session-start.mjs +171 -49
- package/hooks/post-bash-issue-budget-refund.mjs +375 -0
- package/hooks/pre-auq-clarity.mjs +70 -18
- package/hooks/pre-bash-issue-budget.mjs +170 -26
- package/hooks/subagent-telemetry.mjs +106 -20
- package/package.json +5 -4
- package/pi/prompts/ux-grill.md +12 -0
- package/scripts/baseline-archetypes.mjs +28 -0
- package/scripts/ci/assert-vitest-green.mjs +4 -2
- package/scripts/dialectic-deriver.mjs +32 -8
- package/scripts/emit-session.mjs +72 -1
- package/scripts/lib/agent-status.mjs +441 -9
- package/scripts/lib/auq/schema.mjs +10 -3
- package/scripts/lib/auto-dialectic.mjs +0 -68
- package/scripts/lib/baseline-archetypes.mjs +439 -0
- package/scripts/lib/build-live-signals.mjs +5 -6
- package/scripts/lib/ci-status-banner.mjs +29 -6
- package/scripts/lib/claude-md-budget-lint.mjs +52 -2
- package/scripts/lib/config/issue-budget.mjs +68 -8
- package/scripts/lib/config/private-config-dir.mjs +3 -2
- package/scripts/lib/config/remote-hosts.mjs +2 -2
- package/scripts/lib/config-schema.mjs +79 -0
- package/scripts/lib/config.mjs +12 -1
- package/scripts/lib/eval/engine.mjs +7 -1
- package/scripts/lib/file-lock.mjs +151 -8
- package/scripts/lib/git-porcelain.mjs +113 -0
- package/scripts/lib/instruction-budget-guard.mjs +415 -47
- package/scripts/lib/io.mjs +29 -4
- package/scripts/lib/issue-budget-reconcile.mjs +392 -0
- package/scripts/lib/issue-budget.mjs +412 -9
- package/scripts/lib/learnings/evolve-telemetry.mjs +1 -2
- package/scripts/lib/learnings/sizing-subject.mjs +44 -0
- package/scripts/lib/locks/staging-fence-lock.mjs +19 -38
- package/scripts/lib/locks/state-md-lock.mjs +19 -41
- package/scripts/lib/maintenance-due-banner.mjs +450 -0
- package/scripts/lib/owner-config.example.yaml +29 -46
- package/scripts/lib/owner-yaml.mjs +14 -13
- package/scripts/lib/peer-cards/merger.mjs +143 -0
- package/scripts/lib/pre-dispatch-check.mjs +20 -14
- package/scripts/lib/project-hygiene.mjs +81 -30
- package/scripts/lib/quality-gate.mjs +27 -71
- package/scripts/lib/reconcile/engine.mjs +19 -1
- package/scripts/lib/reconcile/writer.mjs +278 -11
- package/scripts/lib/resource-probe/evaluate.mjs +19 -21
- package/scripts/lib/rules-sync.mjs +34 -4
- package/scripts/lib/scope-echo.mjs +346 -0
- package/scripts/lib/session-close-backfill.mjs +182 -40
- package/scripts/lib/session-end/phase-skip.mjs +85 -86
- package/scripts/lib/session-end/tail-runner.mjs +178 -0
- package/scripts/lib/session-lock.mjs +62 -2
- package/scripts/lib/session-record-repair.mjs +91 -0
- package/scripts/lib/session-schema/constants.mjs +6 -0
- package/scripts/lib/session-schema/filters.mjs +26 -1
- package/scripts/lib/session-schema/validator.mjs +20 -0
- package/scripts/lib/session-shape.mjs +558 -0
- package/scripts/lib/session-start-probes.mjs +429 -56
- package/scripts/lib/session-token-rollup.mjs +95 -10
- package/scripts/lib/state-md/frontmatter-mutators.mjs +22 -34
- package/scripts/lib/state-md.mjs +1 -0
- package/scripts/lib/subagents-schema.mjs +77 -9
- package/scripts/lib/telemetry/pricing.mjs +197 -0
- package/scripts/lib/telemetry/sync.mjs +50 -1
- package/scripts/lib/test-runner/artifact-paths.mjs +30 -5
- package/scripts/lib/test-runner/issue-reconcile.mjs +45 -8
- package/scripts/lib/tmux-layout/layouts.mjs +62 -4
- package/scripts/lib/ux-grill/collect.mjs +1163 -0
- package/scripts/lib/ux-grill/compare.mjs +285 -0
- package/scripts/lib/ux-grill/manifest.mjs +618 -0
- package/scripts/lib/ux-grill/measures.mjs +431 -0
- package/scripts/lib/ux-grill/paths.mjs +224 -0
- package/scripts/lib/ux-grill/pencil-coverage.mjs +284 -0
- package/scripts/lib/ux-grill/reconcile.mjs +344 -0
- package/scripts/lib/ux-grill/run-record.mjs +316 -0
- package/scripts/lib/ux-grill/schema.mjs +321 -0
- package/scripts/lib/validate/check-skill-script-paths.mjs +33 -10
- package/scripts/lib/validate/check-untracked-test-deps.mjs +33 -19
- package/scripts/lib/validate/check-unwired-features.mjs +56 -27
- package/scripts/lib/vault-mirror/process.mjs +2 -1
- package/scripts/lib/vault-status/board-lock.mjs +18 -0
- package/scripts/lib/vault-status/board-writer.mjs +8 -0
- package/scripts/lib/vault-status/narrative-mirror.mjs +4 -4
- package/scripts/lib/wave-resource-gate.mjs +23 -27
- package/scripts/lib/wave-sizing.mjs +10 -3
- package/scripts/materialize-wave-scope.mjs +68 -14
- package/scripts/mcp-server.sh +16 -1
- package/scripts/print-applicable-rules.mjs +7 -6
- package/scripts/print-learnings-index.mjs +3 -2
- package/scripts/release.mjs +7 -2
- package/scripts/session-shape.mjs +266 -0
- package/skills/_shared/config-reading.md +15 -9
- package/skills/_shared/private-capability-context.md +89 -0
- package/skills/bootstrap/SKILL.md +60 -209
- package/skills/bootstrap/_shared-template.md +99 -14
- package/skills/bootstrap/deep-template.md +36 -26
- package/skills/bootstrap/fast-template.md +44 -8
- package/skills/bootstrap/intensity-heuristic.md +10 -4
- package/skills/bootstrap/private-contract.md +119 -0
- package/skills/bootstrap/public-fallback.md +30 -18
- package/skills/bootstrap/references/bootstrap-ecosystem-health-flow.md +48 -0
- package/skills/bootstrap/references/bootstrap-refresh-lock-flow.md +37 -0
- package/skills/bootstrap/references/bootstrap-retroactive-flow.md +108 -0
- package/skills/bootstrap/references/bootstrap-rules-fetch-bridge.md +64 -0
- package/skills/bootstrap/standard-template.md +39 -24
- package/skills/claude-md-drift-check/SKILL.md +9 -2
- package/skills/claude-md-drift-check/checker.mjs +213 -21
- package/skills/discovery/SKILL.md +6 -173
- package/skills/discovery/probes/vault-staleness.mjs +35 -5
- package/skills/discovery/probes-docs.md +8 -4
- package/skills/discovery/probes-supply-chain.md +4 -2
- package/skills/discovery/probes-ui.md +8 -4
- package/skills/discovery/probes-vault.md +12 -4
- package/skills/discovery/references/discovery-interactive-triage.md +139 -0
- package/skills/discovery/references/discovery-triage-state.md +54 -0
- package/skills/docs-orchestrator/audience-mapping.md +1 -1
- package/skills/eval/rubric-v1.md +13 -0
- package/skills/evolve/SKILL.md +2 -458
- package/skills/evolve/references/evolve-analyze-mode.md +360 -0
- package/skills/evolve/references/evolve-dialectic-mode.md +139 -0
- package/skills/gitlab-ops/SKILL.md +3 -3
- package/skills/grill/SKILL.md +1 -1
- package/skills/memory-cleanup/SKILL.md +2 -2
- package/skills/plan/mode-new.md +9 -0
- package/skills/plan/mode-retro.md +4 -3
- package/skills/reconcile/SKILL.md +11 -1
- package/skills/session-end/SKILL.md +3 -2
- package/skills/session-end/drift-operations.md +20 -5
- package/skills/session-end/metrics-collection.md +1 -0
- package/skills/session-end/phase-3-2-docs-verification.md +1 -1
- package/skills/session-end/phase-3-6-tail.md +27 -67
- package/skills/session-end/phase-3-7a-recommendations.md +2 -2
- package/skills/session-end/references/phase-2-quality-gate.md +3 -3
- package/skills/session-end/references/phase-3-documentation-updates.md +8 -6
- package/skills/session-end/references/phase-5-issue-cleanup.md +32 -1
- package/skills/session-end/session-metrics-write.md +33 -12
- package/skills/session-plan/SKILL.md +46 -180
- package/skills/session-plan/references/session-plan-task-classification.md +152 -0
- package/skills/session-plan/wave-template.md +8 -15
- package/skills/session-start/SKILL.md +41 -7
- package/skills/session-start/phase-2-5-docs-planning.md +1 -1
- package/skills/session-start/phase-8-5-express-path.md +12 -9
- package/skills/session-start/references/operations-contract.md +114 -0
- package/skills/session-start/references/phase-1-5-session-continuity.md +2 -0
- package/skills/session-start/references/phase-4-ssot-environment-check.md +42 -24
- package/skills/session-start/references/phase-6-7-memory-banner-telemetry-consent.md +3 -1
- package/skills/session-start/soul.md +2 -2
- package/skills/test-runner/SKILL.md +1 -1
- package/skills/test-runner/rubric-v1.md +2 -2
- package/skills/tmux-layout/SKILL.md +3 -1
- package/skills/ux-grill/SKILL.md +211 -0
- package/skills/ux-grill/rubric-v2.md +201 -0
- package/skills/ux-grill/soul.md +76 -0
- package/skills/wave-executor/SKILL.md +32 -127
- package/skills/wave-executor/circuit-breaker.md +3 -1
- package/skills/wave-executor/references/wave-executor-quality-gate.md +61 -0
- package/skills/wave-executor/references/wave-executor-state-init.md +86 -0
- package/skills/wave-executor/references/wave-loop-dispatch.md +12 -2
- package/skills/wave-executor/references/wave-loop-review.md +19 -6
- package/skills/wave-executor/references/wave-loop-scope-manifest.md +6 -2
- package/templates/_shared/ux-manifest.template.md +149 -0
- package/templates/nextjs-minimal/package.json +1 -1
- package/templates/node-minimal/package.json +1 -1
- package/scripts/lib/multi-provider-build/providers.mjs +0 -64
- package/scripts/lib/multi-provider-build/templating.mjs +0 -130
- package/scripts/lib/owner-config/coerce.mjs +0 -29
- package/scripts/lib/owner-config/constants.mjs +0 -21
- package/scripts/lib/owner-config/defaults.mjs +0 -50
- package/scripts/lib/owner-config/error.mjs +0 -19
- package/scripts/lib/owner-config/index.mjs +0 -13
- package/scripts/lib/owner-config/merge.mjs +0 -52
- package/scripts/lib/owner-config/validate.mjs +0 -259
- package/scripts/lib/owner-config-loader.mjs +0 -170
- package/scripts/lib/owner-config.mjs +0 -28
- package/scripts/lib/soul-resolve.mjs +0 -130
- package/scripts/lib/vault-mirror/render.mjs +0 -8
- package/templates/_shared/journey-manifest.md +0 -114
|
@@ -551,6 +551,21 @@ function lookupIssueState(iid, repo, cache, vaultDir) {
|
|
|
551
551
|
// of being silently skipped, since a completeness audit that silently
|
|
552
552
|
// drops files defeats its purpose. WARN, not error — an unreadable file
|
|
553
553
|
// must not brick the gate under `mode: hard`.
|
|
554
|
+
// 6. fleet-intent-glob → notes[]: a glob DECLARED as fleet intent in the
|
|
555
|
+
// rule's own `fleet-intent-globs:` frontmatter key — reported with its
|
|
556
|
+
// local match count, never warned, and it settles probes 3+4 for that
|
|
557
|
+
// pattern. See FLEET_INTENT_FM_KEY below.
|
|
558
|
+
//
|
|
559
|
+
// This list is numeric, not a call sequence — the code below does NOT run
|
|
560
|
+
// 1→2→3→4→5→6 in order. Per rule file: probe 5's read-failure guard runs
|
|
561
|
+
// FIRST (it gates every other probe via `continue` on an unreadable file,
|
|
562
|
+
// before probe 1 ever sees the content), then probe 1, then probes 3/4 with
|
|
563
|
+
// probe 6 interleaved INSIDE the same per-pattern loop (it must intercept a
|
|
564
|
+
// declared fleet-intent pattern before 3/4 evaluate it), then probe 2b: once
|
|
565
|
+
// every rule file has been visited, probe 2a runs last over CLAUDE.md/
|
|
566
|
+
// AGENTS.md. `grep -n "Probe [0-9]" checker.mjs` therefore surfaces markers
|
|
567
|
+
// in code order (5, 1, 6, 2b, 2a), not numeric order — that is intentional,
|
|
568
|
+
// not drift; do not "fix" it by moving probe 6's marker next to probe 5's.
|
|
554
569
|
// ───────────────────────────────────────────────────────────────────────────
|
|
555
570
|
|
|
556
571
|
let _picomatchRuleScoping = null;
|
|
@@ -642,6 +657,95 @@ function extractFrontmatterBlockBody(content) {
|
|
|
642
657
|
* foreign-glob probe's discriminator, per Check 9 spec. */
|
|
643
658
|
const FOREIGN_GLOB_TOKEN_RE = /[A-Z][a-z]+[A-Z]/;
|
|
644
659
|
|
|
660
|
+
// ───────────────────────────────────────────────────────────────────────────
|
|
661
|
+
// Fleet-intent globs (Probe 6) — an exemption for globs that are DELIBERATELY
|
|
662
|
+
// zero-match in THIS repo because the rule ships to consumer repos with a
|
|
663
|
+
// different language convention.
|
|
664
|
+
//
|
|
665
|
+
// Why this exists: `.claude/rules/testing.md` carries `**/*Tests*` — the
|
|
666
|
+
// Java/C#/Swift test-naming convention. This repo is pure `.mjs`
|
|
667
|
+
// (`*.test.mjs`), so the pattern matches 0 tracked files here and the
|
|
668
|
+
// zero-match probe warned on it on every run. On 2026-09-09 an agent acted on
|
|
669
|
+
// exactly that warning and DELETED the glob; `tests/skills/config-reading-glob-rules.test.mjs`
|
|
670
|
+
// (which pins it as fleet intent, #445) went red and the deletion was reverted
|
|
671
|
+
// coordinator-direct. A warning whose only available action is "click away"
|
|
672
|
+
// trains clicking away — and here it had already caused the removal of the
|
|
673
|
+
// thing it was meant to protect (`.claude/rules/host-resources.md` § HR-101).
|
|
674
|
+
//
|
|
675
|
+
// The fix is CATEGORY SEPARATION, not suppression (`development.md` § Guard &
|
|
676
|
+
// Threshold Design): a declared fleet-intent glob is REPORTED in `notes[]`
|
|
677
|
+
// ("0 local matches, declared fleet intent — in order") instead of being
|
|
678
|
+
// warned about. Nothing is silenced; the finding simply stops asking for an
|
|
679
|
+
// action that does not exist.
|
|
680
|
+
//
|
|
681
|
+
// Declaration lives WITH the rule, as a frontmatter list:
|
|
682
|
+
//
|
|
683
|
+
// ---
|
|
684
|
+
// globs:
|
|
685
|
+
// - "**/*Tests*"
|
|
686
|
+
// fleet-intent-globs:
|
|
687
|
+
// - "**/*Tests*" # Java/C#/Swift convention — matches in consumer repos
|
|
688
|
+
// ---
|
|
689
|
+
//
|
|
690
|
+
// rule-loader.mjs's parseGlobsFrontmatter ignores unknown top-level keys (and
|
|
691
|
+
// skips their indented continuation lines), so the key is inert for rule
|
|
692
|
+
// loading. The declaration is per-pattern on purpose: marking one glob must
|
|
693
|
+
// never amnesty a genuinely dead sibling glob in the same rule.
|
|
694
|
+
const FLEET_INTENT_FM_KEY = 'fleet-intent-globs';
|
|
695
|
+
|
|
696
|
+
function stripYamlQuotes(s) {
|
|
697
|
+
return s.replace(/^["']|["']$/g, '');
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
/**
|
|
701
|
+
* Parses the `fleet-intent-globs:` frontmatter key out of a raw frontmatter
|
|
702
|
+
* block body. Accepts block style (`key:` + indented ` - value` lines),
|
|
703
|
+
* flow style (`key: ["a", "b"]`) and a single inline value.
|
|
704
|
+
*
|
|
705
|
+
* @param {string|null} fmBody - output of extractFrontmatterBlockBody()
|
|
706
|
+
* @returns {string[]} declared patterns (possibly empty)
|
|
707
|
+
*/
|
|
708
|
+
function parseFleetIntentGlobs(fmBody) {
|
|
709
|
+
if (!fmBody) return [];
|
|
710
|
+
const out = [];
|
|
711
|
+
let inBlock = false;
|
|
712
|
+
for (const raw of fmBody.split(/\r?\n/)) {
|
|
713
|
+
const line = raw.replace(/\s+$/, '');
|
|
714
|
+
if (line === '' || /^\s*#/.test(line)) continue;
|
|
715
|
+
if (inBlock) {
|
|
716
|
+
const seq = line.match(/^\s+-\s+(.*)$/);
|
|
717
|
+
if (seq) { out.push(stripYamlQuotes(seq[1].trim())); continue; }
|
|
718
|
+
inBlock = false;
|
|
719
|
+
}
|
|
720
|
+
if (/^\s/.test(line)) continue; // another block's continuation
|
|
721
|
+
const colonIdx = line.indexOf(':');
|
|
722
|
+
if (colonIdx === -1) continue;
|
|
723
|
+
if (line.slice(0, colonIdx).trim() !== FLEET_INTENT_FM_KEY) continue;
|
|
724
|
+
const value = line.slice(colonIdx + 1).trim();
|
|
725
|
+
if (value === '') { inBlock = true; continue; }
|
|
726
|
+
if (value.startsWith('[') && value.endsWith(']')) {
|
|
727
|
+
const inner = value.slice(1, -1).trim();
|
|
728
|
+
if (inner) out.push(...inner.split(',').map((s) => stripYamlQuotes(s.trim())));
|
|
729
|
+
continue;
|
|
730
|
+
}
|
|
731
|
+
out.push(stripYamlQuotes(value));
|
|
732
|
+
}
|
|
733
|
+
return out.filter(Boolean);
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
/**
|
|
737
|
+
* Resolves whether `pattern` is a declared fleet-intent glob. The rule file's
|
|
738
|
+
* own `fleet-intent-globs:` frontmatter is the ONLY source — knowledge about a
|
|
739
|
+
* rule stays with the rule. (The one-entry built-in migration seed for the
|
|
740
|
+
* #445 `testing.md` / `**\/*Tests*` case was removed once that file declared
|
|
741
|
+
* the key itself; no checker-side exemption list exists any more.)
|
|
742
|
+
*
|
|
743
|
+
* @returns {'frontmatter'|null} declaration source, or null
|
|
744
|
+
*/
|
|
745
|
+
function fleetIntentSource(pattern, declaredSet) {
|
|
746
|
+
return declaredSet.has(pattern) ? 'frontmatter' : null;
|
|
747
|
+
}
|
|
748
|
+
|
|
645
749
|
/**
|
|
646
750
|
* Extracts bare `<name>.md` tokens from a "## See Also" footer's body lines,
|
|
647
751
|
* skipping any token that carries a path separator (cross-directory
|
|
@@ -819,30 +923,60 @@ function main() {
|
|
|
819
923
|
|
|
820
924
|
const errors = [];
|
|
821
925
|
const warnings = [];
|
|
926
|
+
// Informational findings — never block, never count as a warning. Currently
|
|
927
|
+
// fed only by rule-scoping's fleet-intent-glob probe (see FLEET_INTENT_FM_KEY
|
|
928
|
+
// above). Additive JSON key: consumers reading `errors`/`warnings` are
|
|
929
|
+
// unaffected.
|
|
930
|
+
const notes = [];
|
|
822
931
|
const issueCache = new Map();
|
|
823
932
|
|
|
824
933
|
// Check 6: session-config-parity (issue #30) — diff top-level keys under
|
|
825
934
|
// `## Session Config` between the canonical template and the local
|
|
826
935
|
// instruction file.
|
|
827
936
|
//
|
|
828
|
-
//
|
|
829
|
-
//
|
|
830
|
-
//
|
|
831
|
-
// OPT-IN key (present only in the "Full opt-in baseline"
|
|
832
|
-
//
|
|
833
|
-
//
|
|
834
|
-
//
|
|
835
|
-
//
|
|
836
|
-
//
|
|
837
|
-
//
|
|
937
|
+
// Three-category split (issue #30 → #785 → #1356):
|
|
938
|
+
// · missing MANDATORY key (present in the template's "Full minimal
|
|
939
|
+
// baseline" — the schema-enforced keys) → errors[] (unchanged)
|
|
940
|
+
// · missing OPT-IN key (present only in the "Full opt-in baseline")
|
|
941
|
+
// → notes[] (#1356)
|
|
942
|
+
// · local key UNKNOWN to the template union → warnings[] (#1356)
|
|
943
|
+
//
|
|
944
|
+
// #785's follow-up put the opt-in gap in `warnings[]`. Measured 2026-09-13
|
|
945
|
+
// @ ff1ed191 that produced 36 of this repo's 36 config-parity warnings — a
|
|
946
|
+
// whole warning class firing on every single run, each entry saying "not
|
|
947
|
+
// required" and offering no action. `.claude/rules/host-resources.md`
|
|
948
|
+
// § HR-101 calls that a broken instrument; the repair is CATEGORY
|
|
949
|
+
// SEPARATION, never suppression and never a threshold (`development.md`
|
|
950
|
+
// § Guard & Threshold Design), exactly as the fleet-intent-glob probe did
|
|
951
|
+
// for rule-scoping (#1312). So a deliberately-unadopted opt-in feature is
|
|
952
|
+
// now REPORTED in `notes[]` — still emitted, still rendered (session-end's
|
|
953
|
+
// drift-operations.md renders `notes[]` under its own heading), but no
|
|
954
|
+
// longer asking for an action that does not exist.
|
|
955
|
+
//
|
|
956
|
+
// What replaces it in `warnings[]` is the direction nothing inspected
|
|
957
|
+
// before: a top-level key in the LOCAL Session Config that the template's
|
|
958
|
+
// `## Session Config` blocks do not carry. That one IS actionable — it is
|
|
959
|
+
// either a typo/rename, or the template's baseline blocks have not caught
|
|
960
|
+
// up, and both are fixed by an edit.
|
|
838
961
|
//
|
|
839
|
-
//
|
|
840
|
-
//
|
|
841
|
-
//
|
|
842
|
-
//
|
|
843
|
-
//
|
|
844
|
-
//
|
|
845
|
-
//
|
|
962
|
+
// NAME THE POPULATION (fix-pass 2026-09-13). This check reads exactly ONE
|
|
963
|
+
// population: the template's `## Session Config` blocks. It therefore
|
|
964
|
+
// CANNOT know whether the key is documented elsewhere in the template, nor
|
|
965
|
+
// whether anything reads it at runtime — and #1356's first wording asserted
|
|
966
|
+
// both. Measured 2026-09-13 @ ff1ed191 in this repo, it fired on three keys
|
|
967
|
+
// and was wrong about two of them:
|
|
968
|
+
// awk '/^## /{sec=$0} /^auto-skill-dispatch:|^issue-budget:/{print NR,sec}' \
|
|
969
|
+
// docs/session-config-template.md
|
|
970
|
+
// → auto-skill-dispatch line 87 (## Auto-Skill Dispatch)
|
|
971
|
+
// → issue-budget line 123 (## Issue Budget)
|
|
972
|
+
// rg -c "'auto-skill-dispatch'" scripts/ → 4 files, non-zero
|
|
973
|
+
// rg -c "'issue-budget'" scripts/ → non-zero
|
|
974
|
+
// Both are documented in the template FILE and both are read at runtime;
|
|
975
|
+
// only their absence from the two `## Session Config` blocks (lines 658/676)
|
|
976
|
+
// was ever true. The message below now asserts only that.
|
|
977
|
+
//
|
|
978
|
+
// `mode: hard`/`strict` still keys off `errors[]` only, so neither notes
|
|
979
|
+
// nor the new warnings can block `runConfigValidationGate()`.
|
|
846
980
|
let configParityRan = false;
|
|
847
981
|
if (!args.skipSessionConfigParity) {
|
|
848
982
|
const templatePath = args.configTemplate
|
|
@@ -887,9 +1021,26 @@ function main() {
|
|
|
887
1021
|
extracted: key,
|
|
888
1022
|
});
|
|
889
1023
|
} else {
|
|
1024
|
+
notes.push({
|
|
1025
|
+
check: 'session-config-parity', probe: 'opt-in-gap', file: rel, line,
|
|
1026
|
+
message: `Session Config omits opt-in top-level key '${key}' (documented in docs/session-config-template.md's opt-in baseline; not required). Reported, not warned.`,
|
|
1027
|
+
extracted: key,
|
|
1028
|
+
});
|
|
1029
|
+
}
|
|
1030
|
+
}
|
|
1031
|
+
// Actionable direction (#1356): a local top-level key the template
|
|
1032
|
+
// union never documents. Only reachable when the template HAS an
|
|
1033
|
+
// opt-in block distinct from the minimal one — a single-block
|
|
1034
|
+
// template (test fixture, or a repo that never split the baselines)
|
|
1035
|
+
// is not a key catalog, so diffing against it would warn on every
|
|
1036
|
+
// legitimately-adopted key.
|
|
1037
|
+
if (tplMinimalBlock && tplBlock.headingLine !== tplMinimalBlock.headingLine) {
|
|
1038
|
+
const tplKeySet = new Set(tplKeys);
|
|
1039
|
+
for (const key of localKeys) {
|
|
1040
|
+
if (tplKeySet.has(key)) continue;
|
|
890
1041
|
warnings.push({
|
|
891
1042
|
check: 'session-config-parity', file: rel, line,
|
|
892
|
-
message: `Session Config
|
|
1043
|
+
message: `Session Config declares top-level key '${key}' which is absent from both '## Session Config' blocks of docs/session-config-template.md (the only population this check reads) — a typo/rename, or template baseline blocks that have not caught up. This check does not inspect the rest of the template file or any runtime reader, so the key may well be documented elsewhere and in use`,
|
|
893
1044
|
extracted: key,
|
|
894
1045
|
});
|
|
895
1046
|
}
|
|
@@ -1127,7 +1278,7 @@ function main() {
|
|
|
1127
1278
|
|
|
1128
1279
|
// Check 9: rule-scoping — validates .claude/rules/*.md frontmatter against
|
|
1129
1280
|
// the rule-loader.mjs contract (see the doc-comment above the helper
|
|
1130
|
-
// functions for the
|
|
1281
|
+
// functions for the six probes, incl. unreadable-file and fleet-intent-glob). Silently skipped (no id pushed, no
|
|
1131
1282
|
// checksSkipped entry) when .claude/rules/ is absent — mirrors Check 8's
|
|
1132
1283
|
// silent-skip semantics. `--skip-rule-scoping` disables the whole check.
|
|
1133
1284
|
if (!args.skipRuleScoping) {
|
|
@@ -1149,6 +1300,11 @@ function main() {
|
|
|
1149
1300
|
const absPath = join(rulesDir, fname);
|
|
1150
1301
|
const relPath = relative(vaultDir, absPath);
|
|
1151
1302
|
let content;
|
|
1303
|
+
// --- Probe 5: unreadable-file → warnings[] ---
|
|
1304
|
+
// Runs BEFORE probe 1 despite its higher number: it is the per-file
|
|
1305
|
+
// guard that must decide whether any other probe gets to see this
|
|
1306
|
+
// file's content at all. See the "Rule-scoping family" header
|
|
1307
|
+
// comment above for why the numbering and the code order differ.
|
|
1152
1308
|
try {
|
|
1153
1309
|
content = readFileSync(absPath, 'utf8');
|
|
1154
1310
|
} catch (err) {
|
|
@@ -1190,7 +1346,25 @@ function main() {
|
|
|
1190
1346
|
const globs = parsed.globs;
|
|
1191
1347
|
if (Array.isArray(globs) && globs.length > 0) {
|
|
1192
1348
|
if (trackedFiles === null) trackedFiles = listTrackedFiles(vaultDir);
|
|
1349
|
+
const declaredFleetIntent = new Set(parseFleetIntentGlobs(fmBody));
|
|
1193
1350
|
for (const pattern of globs) {
|
|
1351
|
+
// --- Probe 6: fleet-intent-glob → notes[] ---
|
|
1352
|
+
// A DECLARED fleet-intent glob is reported, never warned: it is
|
|
1353
|
+
// zero-match here on purpose and serves consumer repos. The
|
|
1354
|
+
// declaration also settles the foreign-glob probe for the same
|
|
1355
|
+
// pattern — a pattern the rule author declared is by definition
|
|
1356
|
+
// not a copy-paste leftover.
|
|
1357
|
+
const fleetSource = fleetIntentSource(pattern, declaredFleetIntent);
|
|
1358
|
+
if (fleetSource !== null) {
|
|
1359
|
+
const matchesLocally = globMatchesAny(pattern, trackedFiles);
|
|
1360
|
+
notes.push({
|
|
1361
|
+
check: 'rule-scoping', probe: 'fleet-intent-glob', file: relPath, line: 1,
|
|
1362
|
+
message: `glob '${pattern}' matches ${matchesLocally ? '≥1' : '0'} tracked files here and is declared FLEET INTENT (${fleetSource}) — it serves consumer repos with a different language convention. Reported, not warned.`,
|
|
1363
|
+
extracted: pattern,
|
|
1364
|
+
source: fleetSource,
|
|
1365
|
+
});
|
|
1366
|
+
continue;
|
|
1367
|
+
}
|
|
1194
1368
|
if (!globMatchesAny(pattern, trackedFiles)) {
|
|
1195
1369
|
warnings.push({
|
|
1196
1370
|
check: 'rule-scoping', file: relPath, line: 1,
|
|
@@ -1415,7 +1589,7 @@ function main() {
|
|
|
1415
1589
|
status: 'skipped', mode: args.mode, vault_dir: vaultDir,
|
|
1416
1590
|
resolved_path: resolvedPath, resolved_kind: resolvedKind,
|
|
1417
1591
|
files_scanned: 0, checks_run: checksRun, checks_skipped: checksSkipped,
|
|
1418
|
-
errors, warnings, reason: 'no scope files matched',
|
|
1592
|
+
errors, warnings, notes, reason: 'no scope files matched',
|
|
1419
1593
|
}) + '\n');
|
|
1420
1594
|
process.exit(errors.length > 0 && args.mode === 'strict' ? 1 : 0);
|
|
1421
1595
|
}
|
|
@@ -1572,11 +1746,29 @@ function main() {
|
|
|
1572
1746
|
files_scanned: scopeFiles.length,
|
|
1573
1747
|
checks_run: checksRun,
|
|
1574
1748
|
checks_skipped: checksSkipped,
|
|
1575
|
-
errors, warnings,
|
|
1749
|
+
errors, warnings, notes,
|
|
1576
1750
|
};
|
|
1577
1751
|
if (actualCommandCount !== null) {
|
|
1578
1752
|
result.command_count = { actual: actualCommandCount };
|
|
1579
1753
|
}
|
|
1754
|
+
// --- notes[] human renderer (#1312) ---
|
|
1755
|
+
// `notes[]` is the REPORT-ONLY category the fleet-intent split created
|
|
1756
|
+
// (.claude/rules/development.md § Guard & Threshold Design: separate the
|
|
1757
|
+
// category, never raise the threshold). Built and returned in the JSON since
|
|
1758
|
+
// that split, it had no renderer — so a note reached no human reader, and an
|
|
1759
|
+
// unread category is the state the split replaced, not a weaker warning.
|
|
1760
|
+
// stdout stays pure JSON (the consumer contract); notes leave on stderr as
|
|
1761
|
+
// diagnostics, under their own heading, and never carry the word "warning"
|
|
1762
|
+
// or "error" — that wording IS the category separation at the output edge.
|
|
1763
|
+
if (notes.length > 0) {
|
|
1764
|
+
process.stderr.write(
|
|
1765
|
+
`NOTES (${notes.length}) \u2014 reported, not warned; no action implied:\n`
|
|
1766
|
+
+ notes
|
|
1767
|
+
.map((n) => ` \u00b7 [${n.check}/${n.probe}] ${n.file}:${n.line} \u2014 ${n.message}\n`)
|
|
1768
|
+
.join(''),
|
|
1769
|
+
);
|
|
1770
|
+
}
|
|
1771
|
+
|
|
1580
1772
|
process.stdout.write(JSON.stringify(result) + '\n');
|
|
1581
1773
|
|
|
1582
1774
|
process.exit(errors.length > 0 && args.mode === 'strict' ? 1 : 0);
|
|
@@ -294,137 +294,11 @@ Present both as structured data in your final output. Do not proceed to Phase 5.
|
|
|
294
294
|
|
|
295
295
|
## Phase 5: Interactive Triage (Standalone Mode Only)
|
|
296
296
|
|
|
297
|
-
|
|
297
|
+
Loads persistent triage state, partitions findings into auto-deferred/high-confidence buckets, and walks the user through Critical/High findings individually and Medium/Low findings in a batch before Phase 6 issue creation.
|
|
298
298
|
|
|
299
|
-
|
|
299
|
+
See [references/discovery-interactive-triage.md](references/discovery-interactive-triage.md).
|
|
300
300
|
|
|
301
|
-
|
|
302
|
-
2. Call `filterFindings({ findings: verifiedFindings, stateMap })` to partition findings into three buckets:
|
|
303
|
-
- `toShow` — state is `open`, `reopened`, or **no prior state entry** (new findings — present for user triage)
|
|
304
|
-
- `suppressed` — state is `dismissed` or `accepted-as-known` (skip silently)
|
|
305
|
-
- `tracked` — state is `promoted-to-#NNN` (issue already filed; show as informational)
|
|
306
|
-
|
|
307
|
-
3. Emit a one-line state banner before the summary table:
|
|
308
|
-
```
|
|
309
|
-
Triage state: [N suppressed] suppressed (dismissed/accepted-as-known), [N tracked] tracked in existing issues. Presenting [N toShow] findings.
|
|
310
|
-
```
|
|
311
|
-
Omit the banner entirely if all three counts are zero (first run).
|
|
312
|
-
|
|
313
|
-
4. Render `tracked` findings as informational lines in the summary — NOT as interactive triage items:
|
|
314
|
-
```
|
|
315
|
-
[INFO] Finding "<title>" (<file_path>) is tracked in #<issue_id> — not re-triaged.
|
|
316
|
-
```
|
|
317
|
-
|
|
318
|
-
5. Continue Phase 5 triage using only `toShow` findings. The `suppressed` bucket requires no user interaction.
|
|
319
|
-
|
|
320
|
-
6. After the user completes triage (Steps 1-4 below), append state changes to `.orchestrator/metrics/discovery-triage.jsonl` via `appendTriageEntry()` from `triage-state.mjs`:
|
|
321
|
-
- User selects "Create issue" → append `{ fingerprint, state: 'promoted-to-#<issue_id>', issue_id: <N>, timestamp, session_id }`
|
|
322
|
-
- User selects "Dismiss -- intentional" or "Dismiss -- false positive" → append `{ fingerprint, state: 'dismissed', user_decision: '<reason>', timestamp, session_id }`
|
|
323
|
-
- User selects "Accept all" for batch → append one `{ fingerprint, state: 'open', ... }` entry per finding (so they re-appear next run if not yet promoted)
|
|
324
|
-
|
|
325
|
-
### 5.1 Auto-Defer Low-Confidence Findings
|
|
326
|
-
|
|
327
|
-
Before presenting findings for triage, separate by confidence threshold:
|
|
328
|
-
|
|
329
|
-
1. Findings with confidence >= threshold → present for interactive triage (below)
|
|
330
|
-
2. Findings with confidence < threshold → auto-defer with summary:
|
|
331
|
-
"Auto-deferred [N] low-confidence findings (score < [threshold]). Review with `/discovery --include-deferred`."
|
|
332
|
-
3. List auto-deferred findings in a collapsed section (not interactive — informational only)
|
|
333
|
-
|
|
334
|
-
### 5.1 Present High-Confidence Findings
|
|
335
|
-
|
|
336
|
-
Present findings using AskUserQuestion -- NEVER plain text options. On Codex CLI where AskUserQuestion is unavailable, present as numbered Markdown lists.
|
|
337
|
-
|
|
338
|
-
Include confidence scores in the presentation:
|
|
339
|
-
```
|
|
340
|
-
[CRITICAL] (confidence: 85) hardcoded-values: API key found in src/config.ts:42
|
|
341
|
-
[HIGH] (confidence: 72) security-basics: eval() usage in src/utils/parser.ts:18
|
|
342
|
-
[MEDIUM] (confidence: 61) orphaned-annotations: TODO without issue in src/lib/auth.ts:55
|
|
343
|
-
```
|
|
344
|
-
|
|
345
|
-
### Step 1: Summary
|
|
346
|
-
|
|
347
|
-
Present a findings overview table:
|
|
348
|
-
|
|
349
|
-
```
|
|
350
|
-
## Discovery Results
|
|
351
|
-
|
|
352
|
-
Probes run: [N] | Findings verified: [N] | False positives discarded: [N]
|
|
353
|
-
|
|
354
|
-
| Category | Critical | High | Medium | Low | Total |
|
|
355
|
-
|----------|----------|------|--------|-----|-------|
|
|
356
|
-
| Code | ... | ... | ... | ... | ... |
|
|
357
|
-
| Infra | ... | ... | ... | ... | ... |
|
|
358
|
-
| UI | ... | ... | ... | ... | ... |
|
|
359
|
-
| Arch | ... | ... | ... | ... | ... |
|
|
360
|
-
| Session | ... | ... | ... | ... | ... |
|
|
361
|
-
| Audit | ... | ... | ... | ... | ... |
|
|
362
|
-
| Vault | ... | ... | ... | ... | ... |
|
|
363
|
-
| Feature | ... | ... | ... | ... | ... |
|
|
364
|
-
```
|
|
365
|
-
|
|
366
|
-
### Step 2: Critical + High Findings -- Review Individually
|
|
367
|
-
|
|
368
|
-
For each Critical or High finding, use AskUserQuestion (on Codex CLI where AskUserQuestion is unavailable, present as numbered Markdown lists):
|
|
369
|
-
|
|
370
|
-
```
|
|
371
|
-
AskUserQuestion({
|
|
372
|
-
questions: [{
|
|
373
|
-
question: "<severity> finding in <file_path> — what should happen with it?",
|
|
374
|
-
header: "Finding",
|
|
375
|
-
options: [
|
|
376
|
-
{ label: "Create issue (<severity>)", description: "Files it as priority::<severity>, so it is tracked outside this session. The code below is copied into the issue body.",
|
|
377
|
-
preview: "<finding title>\n\n<file_path>:<line_number>\n```\n<matched_text with +/-3 lines context>\n```\n\n<description>\n\nRecommended fix: <recommended_fix>" },
|
|
378
|
-
{ label: "Adjust priority", description: "Same issue, a priority you pick — this question then comes back with the new label." },
|
|
379
|
-
{ label: "Dismiss -- intentional", description: "The code is deliberate. Nothing is filed, and the finding stays only in this run's report." },
|
|
380
|
-
{ label: "Dismiss -- false positive", description: "The probe misread the code. Nothing is filed; worth reporting if the same probe misfires again." }
|
|
381
|
-
],
|
|
382
|
-
multiSelect: false
|
|
383
|
-
}]
|
|
384
|
-
})
|
|
385
|
-
```
|
|
386
|
-
|
|
387
|
-
If user selects "Adjust priority", ask which priority with another AskUserQuestion. On Codex CLI where AskUserQuestion is unavailable, present as numbered Markdown lists.
|
|
388
|
-
|
|
389
|
-
### Step 3: Medium + Low Findings -- Review Batched
|
|
390
|
-
|
|
391
|
-
Group remaining findings by category. For each category with medium/low findings (on Codex CLI where AskUserQuestion is unavailable, present as numbered Markdown lists):
|
|
392
|
-
|
|
393
|
-
```
|
|
394
|
-
AskUserQuestion({
|
|
395
|
-
questions: [{
|
|
396
|
-
question: "Create issues for all [N] medium/low findings in [category]?",
|
|
397
|
-
header: "Findings",
|
|
398
|
-
options: [
|
|
399
|
-
{ label: "Accept all (Recommended)", description: "Medium and low findings are cheap to file and cheap to close. Cost: [N] issues, roughly one second apart.",
|
|
400
|
-
preview: "1. [title] -- [file_path]:[line] ([severity])\n2. [title] -- [file_path]:[line] ([severity])\n..." },
|
|
401
|
-
{ label: "Review individually", description: "One question per finding, same options as the critical ones. Cost: [N] more prompts." },
|
|
402
|
-
{ label: "Dismiss all", description: "Nothing is filed for this category. The findings stay in this run's report only." }
|
|
403
|
-
],
|
|
404
|
-
multiSelect: false
|
|
405
|
-
}]
|
|
406
|
-
})
|
|
407
|
-
```
|
|
408
|
-
|
|
409
|
-
If "Review individually" selected, walk through each like Step 2.
|
|
410
|
-
|
|
411
|
-
### Step 4: Batch Confirmation
|
|
412
|
-
|
|
413
|
-
Before creating any issues (on Codex CLI where AskUserQuestion is unavailable, present as numbered Markdown lists):
|
|
414
|
-
|
|
415
|
-
```
|
|
416
|
-
AskUserQuestion({
|
|
417
|
-
questions: [{
|
|
418
|
-
question: "Ready to create [N] issues?\n\n- [X] critical\n- [Y] high\n- [Z] medium\n- [W] low",
|
|
419
|
-
header: "Confirm",
|
|
420
|
-
options: [
|
|
421
|
-
{ label: "Create all [N] issues", description: "Proceed with issue creation" },
|
|
422
|
-
{ label: "Review list first", description: "Show full list before creating" },
|
|
423
|
-
{ label: "Cancel", description: "Do not create any issues" }
|
|
424
|
-
]
|
|
425
|
-
}]
|
|
426
|
-
})
|
|
427
|
-
```
|
|
301
|
+
**Read WHEN:** running in standalone mode (not embedded mode — see Phase 4.6 Embedded Mode Exit above), after Phase 4 scoring completes.
|
|
428
302
|
|
|
429
303
|
## Phase 6: Issue Creation & Report
|
|
430
304
|
|
|
@@ -515,52 +389,11 @@ After Phase 6 (Issue Creation) completes, prepare discovery statistics for sessi
|
|
|
515
389
|
|
|
516
390
|
## Discovery Triage State (#419)
|
|
517
391
|
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
### State File
|
|
521
|
-
|
|
522
|
-
**Location:** `.orchestrator/metrics/discovery-triage.jsonl` (gitignored via `.orchestrator/metrics/*.jsonl` pattern — machine-local, never committed)
|
|
523
|
-
|
|
524
|
-
**Format:** One JSON object per line:
|
|
525
|
-
```json
|
|
526
|
-
{"fingerprint":"aabb1122ccdd3344","state":"dismissed","user_decision":"intentional — debug log","timestamp":"2026-05-17T10:00:00.000Z","session_id":"deep-2"}
|
|
527
|
-
{"fingerprint":"eeff5566aabb7788","state":"promoted-to-#119","issue_id":119,"timestamp":"2026-05-17T10:01:00.000Z","session_id":"deep-2"}
|
|
528
|
-
```
|
|
529
|
-
|
|
530
|
-
### Fingerprint
|
|
531
|
-
|
|
532
|
-
`computeFingerprint({probe, file, severity, ruleId})` → 16-char hex (sha256 prefix).
|
|
533
|
-
|
|
534
|
-
`line_number` is **intentionally excluded** — it drifts on refactoring without the underlying issue changing. A finding is considered "the same" as long as the probe, file path, severity, and ruleId match.
|
|
535
|
-
|
|
536
|
-
### State Enum
|
|
537
|
-
|
|
538
|
-
| State | Meaning |
|
|
539
|
-
|---|---|
|
|
540
|
-
| `open` | Actively needs triage or was explicitly marked for re-review |
|
|
541
|
-
| `dismissed` | User dismissed as intentional or false positive — suppressed on future runs |
|
|
542
|
-
| `accepted-as-known` | Known issue, accepted without creating a VCS issue — suppressed on future runs |
|
|
543
|
-
| `reopened` | Previously suppressed but re-surfaced by user decision — shown again |
|
|
544
|
-
| `promoted-to-#NNN` | VCS issue created; shown informational ("tracked in #NNN") on future runs |
|
|
545
|
-
|
|
546
|
-
### Re-run Semantics
|
|
547
|
-
|
|
548
|
-
On each `/discovery` run, Phase 5 loads the state file and partitions findings before presenting them:
|
|
549
|
-
|
|
550
|
-
- **New findings** (no fingerprint entry) → always shown
|
|
551
|
-
- **`open` or `reopened`** → shown for triage
|
|
552
|
-
- **`dismissed` or `accepted-as-known`** → suppressed (silent — no user interaction needed)
|
|
553
|
-
- **`promoted-to-#NNN`** → informational line only ("tracked in #NNN")
|
|
554
|
-
|
|
555
|
-
A suppressed finding re-appears only if its fingerprint changes — i.e., the probe, file path, severity, or ruleId changes. No TTL on dismissed state.
|
|
392
|
+
The JSONL schema (`.orchestrator/metrics/discovery-triage.jsonl`), fingerprint derivation, state enum (`open`/`dismissed`/`accepted-as-known`/`reopened`), re-run semantics, and the `scripts/lib/discovery/triage-state.mjs` module API that Phase 5 above reads and writes.
|
|
556
393
|
|
|
557
|
-
|
|
394
|
+
See [references/discovery-triage-state.md](references/discovery-triage-state.md).
|
|
558
395
|
|
|
559
|
-
|
|
560
|
-
- `computeFingerprint({probe, file, severity, ruleId}): string`
|
|
561
|
-
- `loadTriageState(stateFilePath?): Promise<Map<fingerprint, entry>>`
|
|
562
|
-
- `appendTriageEntry(stateFilePath, entry): Promise<void>`
|
|
563
|
-
- `filterFindings({findings, stateMap}): {toShow, suppressed, tracked}`
|
|
396
|
+
**Read WHEN:** implementing or debugging Phase 5 triage persistence, or investigating why a finding was suppressed/re-shown across runs.
|
|
564
397
|
|
|
565
398
|
## Anti-Patterns
|
|
566
399
|
|
|
@@ -21,6 +21,15 @@
|
|
|
21
21
|
* reported "stale", 26 of them >7d, with a demonstrably healthy sync chain —
|
|
22
22
|
* because the clock, not the repo, was the denominator.
|
|
23
23
|
*
|
|
24
|
+
* Passive projects (GitLab #1238 point 2): a `01-projects/<slug>/` carrying a
|
|
25
|
+
* `_passive.md` marker is one nobody syncs on purpose (vault convention
|
|
26
|
+
* `_meta/conventions.md`; the same marker is already honoured by
|
|
27
|
+
* `scripts/lib/context-coverage-banner.mjs` via its `COVERAGE_FILES` check).
|
|
28
|
+
* Such a folder is skipped BEFORE any staleness comparison and counted in
|
|
29
|
+
* `metrics.passive_skipped`, so the skip is visible rather than silent — a probe
|
|
30
|
+
* that quietly ignored folders would be indistinguishable from one that found
|
|
31
|
+
* them healthy.
|
|
32
|
+
*
|
|
24
33
|
* Fallback: an overview WITHOUT `lastCommit` carries no repo-activity signal at
|
|
25
34
|
* all, so the wall-clock comparison is the only thing left. It is retained for
|
|
26
35
|
* that case only, marked `basis: 'probe-runtime'` in the evidence and carried at
|
|
@@ -67,6 +76,12 @@ function parseFrontmatter(text) {
|
|
|
67
76
|
// Helpers
|
|
68
77
|
// ---------------------------------------------------------------------------
|
|
69
78
|
|
|
79
|
+
/**
|
|
80
|
+
* Marker file that declares a `01-projects/<slug>/` deliberately un-synced.
|
|
81
|
+
* Same filename `scripts/lib/context-coverage-banner.mjs` accepts as coverage.
|
|
82
|
+
*/
|
|
83
|
+
const PASSIVE_MARKER = '_passive.md';
|
|
84
|
+
|
|
70
85
|
const HOURS_24 = 24 * 60 * 60 * 1000;
|
|
71
86
|
const HOURS_168 = 7 * 24 * 60 * 60 * 1000; // 7 days
|
|
72
87
|
|
|
@@ -89,7 +104,7 @@ export async function runProbe(projectRoot, config) {
|
|
|
89
104
|
|
|
90
105
|
const makeSkip = (skipped_reason) => ({
|
|
91
106
|
findings: [],
|
|
92
|
-
metrics: { scanned_projects: 0, stale_count: 0, errors: 0 },
|
|
107
|
+
metrics: { scanned_projects: 0, stale_count: 0, errors: 0, passive_skipped: 0 },
|
|
93
108
|
duration_ms: Math.round(Date.now() - start),
|
|
94
109
|
skipped_reason,
|
|
95
110
|
});
|
|
@@ -114,7 +129,7 @@ export async function runProbe(projectRoot, config) {
|
|
|
114
129
|
// --- Scan loop ---
|
|
115
130
|
|
|
116
131
|
const findings = [];
|
|
117
|
-
const metrics = { scanned_projects: 0, stale_count: 0, errors: 0 };
|
|
132
|
+
const metrics = { scanned_projects: 0, stale_count: 0, errors: 0, passive_skipped: 0 };
|
|
118
133
|
const now = Date.now();
|
|
119
134
|
|
|
120
135
|
let entries;
|
|
@@ -129,7 +144,7 @@ export async function runProbe(projectRoot, config) {
|
|
|
129
144
|
description: err.message,
|
|
130
145
|
evidence: {},
|
|
131
146
|
}],
|
|
132
|
-
metrics: { scanned_projects: 0, stale_count: 0, errors: 1 },
|
|
147
|
+
metrics: { scanned_projects: 0, stale_count: 0, errors: 1, passive_skipped: 0 },
|
|
133
148
|
duration_ms: Math.round(Date.now() - start),
|
|
134
149
|
error: err.message,
|
|
135
150
|
};
|
|
@@ -138,7 +153,21 @@ export async function runProbe(projectRoot, config) {
|
|
|
138
153
|
for (const entry of entries) {
|
|
139
154
|
if (!entry.isDirectory()) continue;
|
|
140
155
|
|
|
141
|
-
const
|
|
156
|
+
const projectDir = join(projectsDir, entry.name);
|
|
157
|
+
|
|
158
|
+
// #1238 point 2 — a `_passive.md` marker means "nobody syncs this on
|
|
159
|
+
// purpose". Skipped before any comparison, but COUNTED so the skip shows
|
|
160
|
+
// up in the metrics instead of looking like a healthy project. Checked
|
|
161
|
+
// BEFORE the `_overview.md` existence test: a passive folder that carries
|
|
162
|
+
// no overview is still a deliberate skip, and the earlier ordering dropped
|
|
163
|
+
// it into the silent non-project branch, under-reporting the metric. The
|
|
164
|
+
// finding population is unchanged either way — neither branch can emit one.
|
|
165
|
+
if (existsSync(join(projectDir, PASSIVE_MARKER))) {
|
|
166
|
+
metrics.passive_skipped++;
|
|
167
|
+
continue;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const overviewPath = join(projectDir, '_overview.md');
|
|
142
171
|
if (!existsSync(overviewPath)) continue; // non-project dir, skip silently
|
|
143
172
|
|
|
144
173
|
metrics.scanned_projects++;
|
|
@@ -254,6 +283,7 @@ export async function runProbe(projectRoot, config) {
|
|
|
254
283
|
scanned_projects: metrics.scanned_projects,
|
|
255
284
|
stale_count: metrics.stale_count,
|
|
256
285
|
errors: metrics.errors,
|
|
286
|
+
passive_skipped: metrics.passive_skipped,
|
|
257
287
|
duration_ms,
|
|
258
288
|
findings: findings.map(f => ({
|
|
259
289
|
slug: f.evidence.slug,
|
|
@@ -286,7 +316,7 @@ export async function runProbe(projectRoot, config) {
|
|
|
286
316
|
description: err.message,
|
|
287
317
|
evidence: {},
|
|
288
318
|
}],
|
|
289
|
-
metrics: { scanned_projects: 0, stale_count: 0, errors: 1 },
|
|
319
|
+
metrics: { scanned_projects: 0, stale_count: 0, errors: 1, passive_skipped: 0 },
|
|
290
320
|
duration_ms: Math.round(Date.now() - start),
|
|
291
321
|
error: err.message,
|
|
292
322
|
};
|
|
@@ -10,13 +10,15 @@
|
|
|
10
10
|
|
|
11
11
|
```bash
|
|
12
12
|
# Step 1: Verify the probe exists; skip if missing
|
|
13
|
-
|
|
13
|
+
# Probes live in the PLUGIN, not the project: ${PLUGIN_ROOT} is resolved per
|
|
14
|
+
# skills/_shared/config-reading.md. The probe still scans process.cwd() (project root).
|
|
15
|
+
test -f "${PLUGIN_ROOT}/skills/discovery/probes/docs-staleness.mjs" || { echo "SKIPPED: docs-staleness -- ${PLUGIN_ROOT}/skills/discovery/probes/docs-staleness.mjs not found (PLUGIN_ROOT='${PLUGIN_ROOT}'; empty = unresolved, see skills/_shared/config-reading.md)"; exit 0; }
|
|
14
16
|
|
|
15
17
|
# Step 2: Run the probe. It reads docs-staleness.thresholds.living from $CONFIG
|
|
16
18
|
# (passed from the discovery skill) and scans docs/*.md (root level) +
|
|
17
19
|
# docs/examples/*.md for filesystem-mtime staleness.
|
|
18
20
|
node --input-type=module -e "
|
|
19
|
-
import {runProbe} from '
|
|
21
|
+
import {runProbe} from '${PLUGIN_ROOT}/skills/discovery/probes/docs-staleness.mjs';
|
|
20
22
|
const cfg = JSON.parse(process.env.SO_CONFIG || '{}');
|
|
21
23
|
const r = await runProbe(process.cwd(), cfg);
|
|
22
24
|
for (const f of r.findings) {
|
|
@@ -49,14 +51,16 @@ if (r.skipped_reason) console.log('SKIPPED:', r.skipped_reason);
|
|
|
49
51
|
|
|
50
52
|
```bash
|
|
51
53
|
# Step 1: Verify the probe exists; skip if missing
|
|
52
|
-
|
|
54
|
+
# Probes live in the PLUGIN, not the project: ${PLUGIN_ROOT} is resolved per
|
|
55
|
+
# skills/_shared/config-reading.md. The probe still scans process.cwd() (project root).
|
|
56
|
+
test -f "${PLUGIN_ROOT}/skills/discovery/probes/ssot-code-diff.mjs" || { echo "SKIPPED: ssot-code-diff -- ${PLUGIN_ROOT}/skills/discovery/probes/ssot-code-diff.mjs not found (PLUGIN_ROOT='${PLUGIN_ROOT}'; empty = unresolved, see skills/_shared/config-reading.md)"; exit 0; }
|
|
53
57
|
|
|
54
58
|
# Step 2: Run the probe. It reads no config keys — it diffs a hand-curated
|
|
55
59
|
# registry of doc "count" claims (blocked-commands.json rules, .claude/rules/
|
|
56
60
|
# file count, skills/ user-facing directory count, commands/ file count)
|
|
57
61
|
# against the live code/filesystem value each claim describes.
|
|
58
62
|
node --input-type=module -e "
|
|
59
|
-
import {runProbe} from '
|
|
63
|
+
import {runProbe} from '${PLUGIN_ROOT}/skills/discovery/probes/ssot-code-diff.mjs';
|
|
60
64
|
const cfg = JSON.parse(process.env.SO_CONFIG || '{}');
|
|
61
65
|
const r = await runProbe(process.cwd(), cfg);
|
|
62
66
|
for (const f of r.findings) {
|