session-orchestrator 3.22.0 → 3.24.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/.cursor/commands/autopilot-multi.md +14 -0
- package/.cursor/commands/autopilot.md +14 -0
- package/.cursor/commands/bootstrap.md +14 -0
- package/.cursor/commands/brainstorm.md +14 -0
- package/.cursor/commands/close.md +13 -0
- package/.cursor/commands/contract-version-bump.md +14 -0
- package/.cursor/commands/debug.md +14 -0
- package/.cursor/commands/discovery.md +14 -0
- package/.cursor/commands/dispatcher.md +14 -0
- package/.cursor/commands/eli5.md +14 -0
- package/.cursor/commands/eval.md +14 -0
- package/.cursor/commands/evolve.md +14 -0
- package/.cursor/commands/go.md +14 -0
- package/.cursor/commands/grill.md +14 -0
- package/.cursor/commands/harness-audit.md +13 -0
- package/.cursor/commands/journey-audit.md +14 -0
- package/.cursor/commands/memory-cleanup.md +14 -0
- package/.cursor/commands/persona-panel.md +14 -0
- package/.cursor/commands/plan.md +14 -0
- package/.cursor/commands/portfolio.md +14 -0
- package/.cursor/commands/reconcile.md +14 -0
- package/.cursor/commands/release.md +14 -0
- package/.cursor/commands/repo-audit.md +13 -0
- package/.cursor/commands/session.md +14 -0
- package/.cursor/commands/spinout.md +14 -0
- package/.cursor/commands/sunset-review.md +14 -0
- package/.cursor/commands/templates-ack.md +14 -0
- package/.cursor/commands/test.md +14 -0
- package/.cursor/hooks.json +60 -0
- package/.cursor/rules/000-session-orchestrator.mdc +8 -0
- package/.cursor/rules/010-session-workflow.mdc +9 -1
- package/.cursor/rules/020-quality-gates.mdc +1 -1
- package/.cursor/rules/030-wave-execution.mdc +1 -1
- package/.cursor/rules/050-plan.mdc +2 -2
- package/.cursor/rules/070-gitlab-ops.mdc +73 -57
- package/.cursor/rules/080-ecosystem-health.mdc +7 -7
- package/.cursor/skills/architecture/SKILL.md +13 -0
- package/.cursor/skills/autopilot/SKILL.md +12 -0
- package/.cursor/skills/bootstrap/SKILL.md +12 -0
- package/.cursor/skills/brainstorm/SKILL.md +13 -0
- package/.cursor/skills/claude-md-drift-check/SKILL.md +13 -0
- package/.cursor/skills/contract-version-bump/SKILL.md +12 -0
- package/.cursor/skills/convergence-monitoring/SKILL.md +12 -0
- package/.cursor/skills/daily/SKILL.md +12 -0
- package/.cursor/skills/debug/SKILL.md +13 -0
- package/.cursor/skills/discovery/SKILL.md +13 -0
- package/.cursor/skills/dispatcher/SKILL.md +13 -0
- package/.cursor/skills/docs-orchestrator/SKILL.md +13 -0
- package/.cursor/skills/domain-model/SKILL.md +13 -0
- package/.cursor/skills/ecosystem-health/SKILL.md +13 -0
- package/.cursor/skills/eli5/SKILL.md +13 -0
- package/.cursor/skills/eval/SKILL.md +12 -0
- package/.cursor/skills/evolve/SKILL.md +13 -0
- package/.cursor/skills/frontmatter-guard/SKILL.md +13 -0
- package/.cursor/skills/gitlab-ops/SKILL.md +13 -0
- package/.cursor/skills/gitlab-portfolio/SKILL.md +13 -0
- package/.cursor/skills/grill/SKILL.md +13 -0
- package/.cursor/skills/hook-development/SKILL.md +13 -0
- package/.cursor/skills/journey-audit/SKILL.md +13 -0
- package/.cursor/skills/mcp-builder/SKILL.md +13 -0
- package/.cursor/skills/memory-cleanup/SKILL.md +12 -0
- package/.cursor/skills/mode-selector/SKILL.md +13 -0
- package/.cursor/skills/npm-publish/SKILL.md +12 -0
- package/.cursor/skills/peekaboo-driver/SKILL.md +13 -0
- package/.cursor/skills/persona-panel/SKILL.md +12 -0
- package/.cursor/skills/plan/SKILL.md +13 -0
- package/.cursor/skills/playwright-driver/SKILL.md +13 -0
- package/.cursor/skills/quality-gates/SKILL.md +13 -0
- package/.cursor/skills/reconcile/SKILL.md +12 -0
- package/.cursor/skills/remote-offload/SKILL.md +13 -0
- package/.cursor/skills/repo-audit/SKILL.md +13 -0
- package/.cursor/skills/session-end/SKILL.md +13 -0
- package/.cursor/skills/session-plan/SKILL.md +13 -0
- package/.cursor/skills/session-start/SKILL.md +13 -0
- package/.cursor/skills/skill-creator/SKILL.md +13 -0
- package/.cursor/skills/spinout/SKILL.md +12 -0
- package/.cursor/skills/sunset-review/SKILL.md +13 -0
- package/.cursor/skills/test-runner/SKILL.md +13 -0
- package/.cursor/skills/tmux-layout/SKILL.md +13 -0
- package/.cursor/skills/ubiquitous-language/SKILL.md +13 -0
- package/.cursor/skills/using-orchestrator/SKILL.md +13 -0
- package/.cursor/skills/vault-mirror/SKILL.md +13 -0
- package/.cursor/skills/vault-sync/SKILL.md +13 -0
- package/.cursor/skills/wave-executor/SKILL.md +13 -0
- package/.cursor/skills/write-executable-plan/SKILL.md +13 -0
- package/.mcp.json +4 -1
- package/CHANGELOG.md +446 -0
- package/README.md +22 -17
- package/agents/AGENTS.md +23 -4
- package/agents/code-implementer.md +2 -1
- package/agents/db-specialist.md +2 -2
- package/agents/docs-writer.md +3 -1
- package/agents/eval-judge.md +1 -1
- package/agents/session-reviewer.md +7 -1
- package/agents/test-writer.md +2 -1
- package/agents/ui-developer.md +2 -1
- package/commands/bootstrap.md +2 -2
- package/commands/close.md +3 -1
- package/commands/go.md +1 -1
- package/commands/journey-audit.md +43 -0
- package/docs/USER-GUIDE.md +2 -2
- package/docs/ci-setup.md +194 -25
- package/docs/codex-setup.md +64 -0
- package/docs/components.md +7 -7
- package/docs/cursor-setup.md +26 -47
- package/docs/events-schema.md +120 -10
- package/docs/github-mirror-protection.md +197 -0
- package/docs/pi-setup.md +2 -0
- package/docs/rule-authoring.md +3 -1
- package/docs/scope-collision-guard.md +49 -2
- package/docs/session-config-reference.md +89 -9
- package/docs/session-config-template.md +38 -7
- package/docs/telemetry/telemetry-claims.md +11 -10
- package/docs/telemetry.md +52 -1
- package/hooks/_lib/atomic-json.mjs +111 -0
- package/hooks/_lib/lock-bootstrap.mjs +8 -4
- package/hooks/_lib/subagent-paths.mjs +143 -0
- package/hooks/_lib/vcs-create-matcher.mjs +397 -38
- package/hooks/cwd-change-restore.mjs +9 -29
- package/hooks/enforce-scope.mjs +93 -0
- package/hooks/hooks-codex.json +1 -1
- package/hooks/hooks-cursor.json +201 -20
- package/hooks/hooks-pi.json +1 -1
- package/hooks/hooks.json +2 -2
- package/hooks/on-session-end.mjs +486 -19
- package/hooks/on-session-start.mjs +263 -12
- package/hooks/on-stop.mjs +392 -24
- package/hooks/post-bash-write-verify.mjs +104 -4
- package/hooks/post-subagent-discovery-validator.mjs +182 -21
- package/hooks/post-tool-batch-wave-signal.mjs +165 -42
- package/hooks/post-tool-failure-corrective-context.mjs +9 -32
- package/hooks/pre-bash-issue-budget.mjs +117 -4
- package/hooks/pre-bash-memory-propose-audit.mjs +13 -7
- package/hooks/pre-bash-sessions-ledger-guard.mjs +159 -0
- package/hooks/pre-bash-staging-fence.mjs +4 -0
- package/hooks/pre-task-scope-disjoint.mjs +368 -35
- package/hooks/skill-invocation-telemetry.mjs +21 -10
- package/hooks/subagent-telemetry.mjs +11 -26
- package/monitors/monitors.json +6 -0
- package/package.json +1 -1
- package/pi/prompts/journey-audit.md +12 -0
- package/rules/_index.md +9 -1
- package/rules/always-on/ask-via-tool.md +62 -0
- package/rules/always-on/bash-harness-pitfalls.md +168 -0
- package/rules/always-on/build-value.md +47 -0
- package/rules/always-on/cross-session-messaging.md +59 -0
- package/rules/always-on/loop-and-monitor.md +221 -0
- package/rules/always-on/parallel-sessions.md +142 -12
- package/rules/always-on/receiving-review.md +108 -0
- package/rules/always-on/test-value.md +40 -0
- package/rules/always-on/verification-before-completion.md +77 -0
- package/scripts/archive-closed-prds.mjs +258 -18
- package/scripts/autopilot.mjs +31 -12
- package/scripts/backfill-abandoned-sessions.mjs +80 -11
- package/scripts/backfill-evidence-digest.mjs +376 -0
- package/scripts/cursor-install.mjs +89 -48
- package/scripts/emit-event.mjs +10 -2
- package/scripts/export-hw-learnings.mjs +143 -2
- package/scripts/express-path.mjs +299 -0
- package/scripts/generate-cursor-adapter.mjs +253 -0
- package/scripts/github-protection-audit.mjs +358 -0
- package/scripts/lib/auq/parse.mjs +5 -29
- package/scripts/lib/auto-dialectic.mjs +68 -0
- package/scripts/lib/autopilot/worktree-pipeline.mjs +318 -18
- package/scripts/lib/build-live-signals.mjs +49 -27
- package/scripts/lib/ci-status-banner.mjs +158 -11
- package/scripts/lib/cold-start-detector.mjs +23 -14
- package/scripts/lib/command-blocker.mjs +70 -0
- package/scripts/lib/config/block-header.mjs +55 -0
- package/scripts/lib/config/discovery-validator.mjs +7 -2
- package/scripts/lib/config/health-endpoints.mjs +383 -0
- package/scripts/lib/config/reconcile.mjs +79 -4
- package/scripts/lib/config/remote-hosts.mjs +233 -0
- package/scripts/lib/config/section-extractor.mjs +235 -36
- package/scripts/lib/config-schema.mjs +9 -1
- package/scripts/lib/config.mjs +87 -8
- package/scripts/lib/convergence-monitor.mjs +13 -2
- package/scripts/lib/cursor-hook-bridge.mjs +443 -0
- package/scripts/lib/dispatcher/cli.mjs +2 -2
- package/scripts/lib/dispatcher/enumerate.mjs +2 -17
- package/scripts/lib/events-schema.mjs +48 -0
- package/scripts/lib/events.mjs +238 -5
- package/scripts/lib/evolve/autonomy-verdict.mjs +9 -4
- package/scripts/lib/evolve/autopilot-effectiveness.mjs +18 -1
- package/scripts/lib/express-path.mjs +327 -0
- package/scripts/lib/file-lock.mjs +22 -4
- package/scripts/lib/gates/gate-full.mjs +81 -8
- package/scripts/lib/gates/gate-helpers.mjs +76 -15
- package/scripts/lib/git-config-drift.mjs +134 -5
- package/scripts/lib/gitlab-portfolio/cli.mjs +3 -15
- package/scripts/lib/harness-audit/categories/category1.mjs +17 -6
- package/scripts/lib/host-identity.mjs +247 -2
- package/scripts/lib/instruction-budget-guard.mjs +31 -1
- package/scripts/lib/issue-budget.mjs +229 -30
- package/scripts/lib/learnings/io.mjs +55 -10
- package/scripts/lib/learnings/schema.mjs +95 -28
- package/scripts/lib/lock-reaper.mjs +7 -1
- package/scripts/lib/locks/staging-fence-lock.mjs +5 -1
- package/scripts/lib/locks/state-md-lock.mjs +8 -1
- package/scripts/lib/memory-banner.mjs +25 -10
- package/scripts/lib/memory-paths.mjs +15 -6
- package/scripts/lib/mode-selector/scoring.mjs +53 -6
- package/scripts/lib/peer-discovery.mjs +20 -2
- package/scripts/lib/platform.mjs +72 -9
- package/scripts/lib/plugin-root.mjs +143 -19
- package/scripts/lib/project-hygiene.mjs +43 -3
- package/scripts/lib/quality-gate.mjs +271 -13
- package/scripts/lib/reconcile/emitter.mjs +87 -19
- package/scripts/lib/reconcile/engine.mjs +517 -18
- package/scripts/lib/reconcile/idempotency.mjs +102 -1
- package/scripts/lib/reconcile/renderer.mjs +148 -3
- package/scripts/lib/reconcile/sanitize.mjs +40 -17
- package/scripts/lib/reconcile/writer.mjs +415 -84
- package/scripts/lib/rule-loader.mjs +37 -2
- package/scripts/lib/rules-sync.mjs +51 -8
- package/scripts/lib/scope-gate.mjs +126 -0
- package/scripts/lib/session-close-backfill.mjs +427 -37
- package/scripts/lib/session-discovery.mjs +69 -5
- package/scripts/lib/session-end/phase-skip.mjs +38 -5
- package/scripts/lib/session-end/worktree-cleanup.mjs +154 -7
- package/scripts/lib/session-id.mjs +30 -14
- package/scripts/lib/session-identity/own-session.mjs +220 -0
- package/scripts/lib/session-lock.mjs +85 -30
- package/scripts/lib/session-schema/normalizer.mjs +70 -3
- package/scripts/lib/session-schema/validator.mjs +40 -0
- package/scripts/lib/session-start-probes.mjs +608 -0
- package/scripts/lib/session-transition.mjs +277 -0
- package/scripts/lib/sessions-canonical.mjs +446 -0
- package/scripts/lib/sessions-staleness-banner.mjs +124 -57
- package/scripts/lib/spiral-carryover.mjs +90 -9
- package/scripts/lib/state-md/frontmatter-mutators.mjs +41 -8
- package/scripts/lib/state-md/mission-status.mjs +350 -52
- package/scripts/lib/state-md/yaml-parser.mjs +145 -16
- package/scripts/lib/state-md.mjs +12 -2
- package/scripts/lib/telemetry/schema.mjs +74 -8
- package/scripts/lib/telemetry/sync.mjs +91 -16
- package/scripts/lib/tmux-layout/telemetry.mjs +14 -2
- package/scripts/lib/validate/check-agents.mjs +66 -0
- package/scripts/lib/validate/check-cursor-adapter.mjs +102 -0
- package/scripts/lib/validate/check-dead-bridge.mjs +24 -2
- package/scripts/lib/validate/check-doc-cli-commands.mjs +25 -65
- package/scripts/lib/validate/check-hooks-emit-event-guard.mjs +370 -0
- package/scripts/lib/validate/check-hooks-symmetry.mjs +29 -63
- package/scripts/lib/validate/check-playwright-mcp-canary.mjs +13 -22
- package/scripts/lib/validate/check-plugin-monitors.mjs +10 -4
- package/scripts/lib/validate/check-skill-script-paths.mjs +436 -0
- package/scripts/lib/validate/check-test-value-bans.mjs +165 -17
- package/scripts/lib/validate/check-untracked-test-deps.mjs +10 -0
- package/scripts/lib/validate/check-unwired-features.mjs +333 -32
- package/scripts/lib/validate/check-validator-registration.mjs +248 -0
- package/scripts/lib/validate/check-vcs-repo-flag.mjs +6 -28
- package/scripts/lib/validate/markdown-fences.mjs +196 -0
- package/scripts/lib/validate/repo-files.mjs +275 -0
- package/scripts/lib/validate-vendored-rules.mjs +229 -7
- package/scripts/lib/vault-mirror/process.mjs +99 -43
- package/scripts/lib/vault-mirror/telemetry.mjs +210 -0
- package/scripts/lib/vault-staleness-banner.mjs +76 -6
- package/scripts/lib/vault-status/board-lock.mjs +185 -0
- package/scripts/lib/vault-status/board-writer.mjs +381 -141
- package/scripts/lib/vault-status/narrative-mirror.mjs +190 -27
- package/scripts/lib/wave-executor/foreign-dispatch.mjs +832 -0
- package/scripts/lib/wave-executor/remote-dispatch.mjs +504 -0
- package/scripts/lib/wave-resource-gate.mjs +127 -7
- package/scripts/lib/wave-transcript-tail.mjs +889 -0
- package/scripts/materialize-wave-scope.mjs +228 -15
- package/scripts/mcp-server.sh +11 -2
- package/scripts/memory-propose.mjs +132 -8
- package/scripts/parse-config.mjs +65 -0
- package/scripts/promote-vault-strict.mjs +4 -15
- package/scripts/site-numbers.mjs +36 -4
- package/scripts/token-audit.sh +9 -2
- package/scripts/validate-plugin.mjs +29 -0
- package/scripts/validate-wave-scope.mjs +67 -0
- package/scripts/vault-consolidate.mjs +3 -11
- package/scripts/vault-integration-watcher.mjs +2 -4
- package/scripts/vault-mirror.mjs +305 -51
- package/skills/_shared/monitor-patterns.md +31 -5
- package/skills/_shared/parallel-aware-auq.md +31 -2
- package/skills/_shared/parallel-aware-preamble.md +19 -4
- package/skills/_shared/platform-tools.md +11 -5
- package/skills/_shared/state-ownership.md +29 -2
- package/skills/autopilot/SKILL.md +5 -1
- package/skills/bootstrap/SKILL.md +3 -3
- package/skills/bootstrap/_shared-template.md +18 -10
- package/skills/bootstrap/deep-template.md +10 -6
- package/skills/bootstrap/fast-template.md +15 -8
- package/skills/bootstrap/standard-template.md +10 -6
- package/skills/claude-md-drift-check/checker.mjs +39 -11
- package/skills/contract-version-bump/SKILL.md +1 -1
- package/skills/dispatcher/SKILL.md +1 -1
- package/skills/ecosystem-health/SKILL.md +4 -1
- package/skills/ecosystem-health/wizard.md +5 -0
- package/skills/evolve/SKILL.md +38 -1
- package/skills/journey-audit/SKILL.md +270 -0
- package/skills/peekaboo-driver/SKILL.md +15 -3
- package/skills/persona-panel/SKILL.md +1 -1
- package/skills/reconcile/SKILL.md +46 -3
- package/skills/remote-offload/SKILL.md +89 -0
- package/skills/session-end/SKILL.md +17 -4
- package/skills/session-end/metrics-collection.md +7 -4
- package/skills/session-end/phase-3-6-tail.md +20 -9
- package/skills/session-end/phase-3-7a-recommendations.md +16 -2
- package/skills/session-plan/SKILL.md +6 -1
- package/skills/session-plan/wave-template.md +1 -0
- package/skills/session-start/SKILL.md +54 -17
- package/skills/session-start/phase-7-5-mode-selector.md +15 -3
- package/skills/session-start/phase-8-5-express-path.md +77 -12
- package/skills/vault-sync/validator.mjs +31 -0
- package/skills/wave-executor/SKILL.md +5 -3
- package/skills/wave-executor/circuit-breaker.md +34 -9
- package/skills/wave-executor/wave-loop.md +143 -22
- package/templates/_shared/journey-manifest.md +110 -0
- package/templates/_shared/rules/parallel-sessions.md +0 -77
|
@@ -44,6 +44,17 @@
|
|
|
44
44
|
* @property {string} created_at - ISO timestamp.
|
|
45
45
|
* @property {string|null} processed_at - terminal stamp (mirrors repair store).
|
|
46
46
|
* @property {string|null} superseded_by
|
|
47
|
+
* @property {string|null} [outcome] - terminal disposition, one of:
|
|
48
|
+
* `'written'` (writer.mjs persisted the rule file), `'already-on-disk'`
|
|
49
|
+
* (engine.mjs found a matching `.claude/rules/` provenance block before
|
|
50
|
+
* proposing — issue #484), or `'rejected'` (the operator declined the
|
|
51
|
+
* proposal in the approval AUQ and writer.mjs archived it — issue #1042).
|
|
52
|
+
* Optional and additive: `isCandidateShape` only requires `learning_key`
|
|
53
|
+
* + `created_at`, so an older record without this field still round-trips
|
|
54
|
+
* through the store unchanged, and nothing BRANCHES on the value —
|
|
55
|
+
* terminality is `processed_at` alone (see {@link isProcessed}), so a
|
|
56
|
+
* pre-#1042 record with no `outcome` keeps behaving exactly as before
|
|
57
|
+
* (absence reads as `'written'`, the only outcome that existed then).
|
|
47
58
|
*/
|
|
48
59
|
|
|
49
60
|
import { mkdirSync, readFileSync, writeFileSync, renameSync } from 'node:fs';
|
|
@@ -265,7 +276,15 @@ export function loadCandidates({ repoRoot, storePath } = {}) {
|
|
|
265
276
|
* True iff `existing` already holds a candidate that shares `candidate`'s
|
|
266
277
|
* `learning_key` AND has a terminal `processed_at` stamp. The reconcile engine
|
|
267
278
|
* uses this to idempotently SKIP re-proposing a learning whose verdict is
|
|
268
|
-
* already terminal.
|
|
279
|
+
* already terminal.
|
|
280
|
+
*
|
|
281
|
+
* Deliberately OUTCOME-AGNOSTIC: `processed_at` alone decides. A rule the
|
|
282
|
+
* operator DECLINED (`outcome: 'rejected'`, issue #1042) is therefore just as
|
|
283
|
+
* terminal as one that was written — an explicit "no" is a verdict, and
|
|
284
|
+
* re-asking every run is how it gets forgotten. By the same token a pre-#1042
|
|
285
|
+
* record carrying no `outcome` at all keeps its old meaning unchanged.
|
|
286
|
+
*
|
|
287
|
+
* Never throws.
|
|
269
288
|
* @param {ReconcileCandidate} candidate - the candidate under consideration.
|
|
270
289
|
* @param {ReconcileCandidate[]} existing - the currently-persisted candidates.
|
|
271
290
|
* @returns {boolean}
|
|
@@ -337,3 +356,85 @@ export function mergeCandidates({ candidates, repoRoot, storePath } = {}) {
|
|
|
337
356
|
const result = writeStore(absPath, merged);
|
|
338
357
|
return { merged, written: result.ok === true, skipped };
|
|
339
358
|
}
|
|
359
|
+
|
|
360
|
+
/**
|
|
361
|
+
* Stamp the persisted candidate for `learningKey` as terminally processed and
|
|
362
|
+
* write it back through {@link mergeCandidates} — the ONLY writer of this
|
|
363
|
+
* store, so this function opens no second write path and no new persisted
|
|
364
|
+
* format: `outcome` is an ordinary field alongside `processed_at`, and the
|
|
365
|
+
* read-side shape guard (`isCandidateShape`) only requires `learning_key` +
|
|
366
|
+
* `created_at`, so it accepts the stamped record unchanged.
|
|
367
|
+
*
|
|
368
|
+
* Looks up the existing record by `learningKey` first, so the write preserves
|
|
369
|
+
* everything the run that proposed it originally recorded (`slug`, `reason`,
|
|
370
|
+
* `confidence`, `created_at`) and only ADDS the terminal stamp. When no record
|
|
371
|
+
* exists yet — e.g. the proposing run used `dryRun` and never merged into the
|
|
372
|
+
* store, or `engine.mjs` discovered the on-disk `.claude/rules/` file directly
|
|
373
|
+
* without ever having proposed it in a prior run — a fresh terminal record is
|
|
374
|
+
* minted from the caller-supplied `fallback*` fields so a LATER run can still
|
|
375
|
+
* dedupe against it.
|
|
376
|
+
*
|
|
377
|
+
* `mergeCandidates`'s own dedupe rule still applies underneath this call: if
|
|
378
|
+
* the existing record is ALREADY terminal (a prior `processed_at`), the merge
|
|
379
|
+
* keeps the existing verdict and this call is a no-op on disk — a terminal
|
|
380
|
+
* verdict is never regressed, even by a second stamp attempt.
|
|
381
|
+
*
|
|
382
|
+
* Never throws (delegates to the never-throwing `loadCandidates`/
|
|
383
|
+
* `mergeCandidates`); on write failure returns `{ written: false, ... }`.
|
|
384
|
+
*
|
|
385
|
+
* @param {Object} [params]
|
|
386
|
+
* @param {string} [params.learningKey] - required; a missing/empty key is a no-op (`{written:false, stamped:null}`).
|
|
387
|
+
* @param {'written'|'already-on-disk'|'rejected'|string} [params.outcome]
|
|
388
|
+
* @param {string} [params.processedAt] - ISO timestamp; defaults to `new Date().toISOString()`.
|
|
389
|
+
* @param {string} [params.fallbackSlug] - used only when no existing record is found.
|
|
390
|
+
* @param {string} [params.fallbackCandidateId] - used only when no existing record is found; defaults to `makeCandidateId(learningKey, fallbackSlug)`.
|
|
391
|
+
* @param {number} [params.fallbackConfidence] - used only when no existing record is found.
|
|
392
|
+
* @param {string} [params.repoRoot]
|
|
393
|
+
* @param {string} [params.storePath]
|
|
394
|
+
* @returns {{ written: boolean, stamped: ReconcileCandidate|null }}
|
|
395
|
+
*/
|
|
396
|
+
export function markCandidateProcessed({
|
|
397
|
+
learningKey,
|
|
398
|
+
outcome,
|
|
399
|
+
processedAt,
|
|
400
|
+
fallbackSlug,
|
|
401
|
+
fallbackCandidateId,
|
|
402
|
+
fallbackConfidence,
|
|
403
|
+
repoRoot,
|
|
404
|
+
storePath,
|
|
405
|
+
} = {}) {
|
|
406
|
+
if (typeof learningKey !== 'string' || learningKey.length === 0) {
|
|
407
|
+
return { written: false, stamped: null };
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
const stampAt =
|
|
411
|
+
typeof processedAt === 'string' && processedAt.length > 0 ? processedAt : new Date().toISOString();
|
|
412
|
+
|
|
413
|
+
const { records: existing } = loadCandidates({ repoRoot, storePath });
|
|
414
|
+
const found = existing.find((rec) => rec && rec.learning_key === learningKey);
|
|
415
|
+
|
|
416
|
+
/** @type {ReconcileCandidate} */
|
|
417
|
+
let stamped;
|
|
418
|
+
if (found) {
|
|
419
|
+
stamped = { ...found, processed_at: stampAt, outcome: typeof outcome === 'string' ? outcome : (found.outcome ?? null) };
|
|
420
|
+
} else {
|
|
421
|
+
const slug = typeof fallbackSlug === 'string' ? fallbackSlug : '';
|
|
422
|
+
stamped = buildCandidate({
|
|
423
|
+
id:
|
|
424
|
+
typeof fallbackCandidateId === 'string' && fallbackCandidateId.length > 0
|
|
425
|
+
? fallbackCandidateId
|
|
426
|
+
: makeCandidateId(learningKey, slug),
|
|
427
|
+
learningKey,
|
|
428
|
+
slug,
|
|
429
|
+
status: 'proposed',
|
|
430
|
+
reason: 'stamped without a prior sidecar record',
|
|
431
|
+
confidence: typeof fallbackConfidence === 'number' ? fallbackConfidence : 0,
|
|
432
|
+
createdAt: stampAt,
|
|
433
|
+
});
|
|
434
|
+
stamped.processed_at = stampAt;
|
|
435
|
+
stamped.outcome = typeof outcome === 'string' ? outcome : null;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
const result = mergeCandidates({ candidates: [stamped], repoRoot, storePath });
|
|
439
|
+
return { written: result.written === true, stamped };
|
|
440
|
+
}
|
|
@@ -43,6 +43,8 @@
|
|
|
43
43
|
|
|
44
44
|
import { createHash } from 'node:crypto';
|
|
45
45
|
|
|
46
|
+
import yaml from 'js-yaml';
|
|
47
|
+
|
|
46
48
|
import { kebab } from '../learnings/kebab.mjs';
|
|
47
49
|
import {
|
|
48
50
|
EVIDENCE_ITEM_MAX_BYTES,
|
|
@@ -64,6 +66,10 @@ import {
|
|
|
64
66
|
/**
|
|
65
67
|
* Short, stable SHA-1 prefix (first 7 hex chars) of an input string.
|
|
66
68
|
*
|
|
69
|
+
* Slug-collision helper ONLY. Deliberately NOT reused for the evidence digest
|
|
70
|
+
* below (#1101): 7 hex chars of SHA-1 is a namespacing device, not a tamper
|
|
71
|
+
* seal, and conflating the two would let a slug change read as a seal break.
|
|
72
|
+
*
|
|
67
73
|
* @param {string} input
|
|
68
74
|
* @returns {string}
|
|
69
75
|
*/
|
|
@@ -71,6 +77,117 @@ function shortHash(input) {
|
|
|
71
77
|
return createHash('sha1').update(String(input)).digest('hex').slice(0, 7);
|
|
72
78
|
}
|
|
73
79
|
|
|
80
|
+
/**
|
|
81
|
+
* Shape of the `evidence-digest` frontmatter scalar (#1101).
|
|
82
|
+
*
|
|
83
|
+
* `sha256-v1:` names BOTH the algorithm and the canonical-input version, so a
|
|
84
|
+
* future change to the input recipe bumps the prefix rather than silently
|
|
85
|
+
* invalidating every seal on disk.
|
|
86
|
+
*
|
|
87
|
+
* A LITERAL COPY of this regex lives in `skills/claude-md-drift-check/checker.mjs`
|
|
88
|
+
* (Check 8). The checker is a standalone skill script; importing across the
|
|
89
|
+
* skill/script boundary would be new coupling for one 30-character literal.
|
|
90
|
+
* `tests/lib/reconcile/renderer.test.mjs` pins the two copies equal.
|
|
91
|
+
*/
|
|
92
|
+
export const EVIDENCE_DIGEST_RE = /^sha256-v1:[0-9a-f]{64}$/;
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Shape of the `evidence-recorded-at` provenance value — an ISO-8601 instant.
|
|
96
|
+
*
|
|
97
|
+
* A learning's `created_at` is machine-written by the learnings store, but it
|
|
98
|
+
* reaches this renderer on an agent-authored record, and it lands on a body
|
|
99
|
+
* line OUTSIDE an inline-code span. A value carrying a newline could forge a
|
|
100
|
+
* sibling provenance line, so a non-conforming value is treated as ABSENT (the
|
|
101
|
+
* empty-string digest component) rather than interpolated. Declining to record
|
|
102
|
+
* an unusable timestamp is the same posture as the three backfill cases whose
|
|
103
|
+
* `created_at` is genuinely unavailable — an invented date would make the
|
|
104
|
+
* digest a reference masquerading as evidence.
|
|
105
|
+
*/
|
|
106
|
+
const EVIDENCE_RECORDED_AT_RE = /^\d{4}-\d{2}-\d{2}T[0-9:.]+(?:Z|[+-]\d{2}:\d{2})$/;
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Compute the `sha256-v1:` evidence digest from its six canonical components.
|
|
110
|
+
*
|
|
111
|
+
* ── Why this is EVIDENCE and not a REFERENCE (#1101) ─────────────────────────
|
|
112
|
+
* `.orchestrator/metrics/learnings.jsonl` is gitignored, so in a fresh clone a
|
|
113
|
+
* rule's `learning-key` resolves to nothing and its provenance is unverifiable.
|
|
114
|
+
* The last component here is the RENDERED `## Evidence` block itself — so a
|
|
115
|
+
* reader holding only the `.md` file re-reads its own `## Evidence` section,
|
|
116
|
+
* re-joins the five header fields from its own `## Provenance` block, hashes,
|
|
117
|
+
* and compares. `learnings.jsonl` is never consulted.
|
|
118
|
+
*
|
|
119
|
+
* The input is the six components joined with `\n`, UTF-8, **no trailing
|
|
120
|
+
* newline**. Order is fixed and versioned by the `sha256-v1:` prefix.
|
|
121
|
+
*
|
|
122
|
+
* @param {object} parts
|
|
123
|
+
* @param {string} parts.learningKey
|
|
124
|
+
* @param {string} parts.learningId - the same `'n/a'` fallback the body emits.
|
|
125
|
+
* @param {string} parts.sourceSession - ditto.
|
|
126
|
+
* @param {string} parts.evidenceRecordedAt - the learning's `created_at`, or
|
|
127
|
+
* `''` when absent/unusable. NEVER a substituted or invented date.
|
|
128
|
+
* @param {string} parts.confidence - `String(metadata.confidence)`.
|
|
129
|
+
* @param {string} parts.evidenceBlock - the EXACT string written to
|
|
130
|
+
* `## Evidence`, verbatim.
|
|
131
|
+
* @returns {string} `sha256-v1:<64 lowercase hex>`
|
|
132
|
+
*/
|
|
133
|
+
export function computeEvidenceDigest({
|
|
134
|
+
learningKey,
|
|
135
|
+
learningId,
|
|
136
|
+
sourceSession,
|
|
137
|
+
evidenceRecordedAt,
|
|
138
|
+
confidence,
|
|
139
|
+
evidenceBlock,
|
|
140
|
+
}) {
|
|
141
|
+
const canonical = [
|
|
142
|
+
String(learningKey ?? ''),
|
|
143
|
+
String(learningId ?? ''),
|
|
144
|
+
String(sourceSession ?? ''),
|
|
145
|
+
String(evidenceRecordedAt ?? ''),
|
|
146
|
+
String(confidence ?? ''),
|
|
147
|
+
String(evidenceBlock ?? ''),
|
|
148
|
+
].join('\n');
|
|
149
|
+
return `sha256-v1:${createHash('sha256').update(canonical, 'utf8').digest('hex')}`;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Serialize a single-line frontmatter scalar the way real YAML requires it
|
|
154
|
+
* (#1041), while staying parseable by `rule-loader.mjs`'s hand-rolled parser.
|
|
155
|
+
*
|
|
156
|
+
* `description` is the one frontmatter field {@link assertSafeDescription}
|
|
157
|
+
* permits to contain a colon (14 live rules carry a second `:` in their
|
|
158
|
+
* description, and this repo's own loader tolerates it — see `sanitize.mjs`'s
|
|
159
|
+
* module doc). This repo's loader is not the only reader: Claude Code's OWN
|
|
160
|
+
* native frontmatter loader parses `.claude/rules/*.md` as real YAML, and
|
|
161
|
+
* `description: fixes X: this breaks Y` emitted unquoted is a YAML mapping
|
|
162
|
+
* with an extra, unindented `this breaks Y` value — js-yaml throws "bad
|
|
163
|
+
* indentation of a mapping entry" on exactly that shape.
|
|
164
|
+
*
|
|
165
|
+
* `yaml.dump` emits a bare plain scalar when no quoting is needed (byte-
|
|
166
|
+
* identical to the un-quoted output this renderer shipped before #1041, so
|
|
167
|
+
* every colon-free description stays unchanged) and falls back to a quoted
|
|
168
|
+
* scalar only when required. js-yaml's DEFAULT quoting style (no
|
|
169
|
+
* `quotingType` option passed) is SINGLE-quote, not double — confirmed via
|
|
170
|
+
* `node_modules/js-yaml/lib/dumper.js` `chooseScalarStyle`, which only forces
|
|
171
|
+
* DOUBLE for a string containing a non-printable code point. A single-quoted
|
|
172
|
+
* scalar is what `rule-loader.mjs`'s hand-rolled parser round-trips
|
|
173
|
+
* correctly: it strips exactly one leading/trailing quote character
|
|
174
|
+
* (`/^["']|["']$/g`) with NO escape processing, so a js-yaml DOUBLE-quoted
|
|
175
|
+
* `\"` escape would survive the strip verbatim and corrupt the parsed value —
|
|
176
|
+
* single-quote style has no such escape to survive (its only escape, an
|
|
177
|
+
* interior `'` doubled to `''`, is a narrower edge case than the colon defect
|
|
178
|
+
* this fix targets and is out of scope here). `description` can never contain
|
|
179
|
+
* a raw newline by the time this runs — {@link assertSafeDescription} /
|
|
180
|
+
* {@link assertNoControlChars} reject one earlier in the same call — so the
|
|
181
|
+
* literal/folded block-scalar branches `yaml.dump` has for multi-line input
|
|
182
|
+
* are unreachable on this call site.
|
|
183
|
+
*
|
|
184
|
+
* @param {string} value
|
|
185
|
+
* @returns {string} a single YAML scalar line, no trailing newline
|
|
186
|
+
*/
|
|
187
|
+
function dumpYamlScalar(value) {
|
|
188
|
+
return yaml.dump(value, { lineWidth: -1 }).trimEnd();
|
|
189
|
+
}
|
|
190
|
+
|
|
74
191
|
/**
|
|
75
192
|
* Derive a kebab-case, stable, idempotent slug for a learning's rule file.
|
|
76
193
|
*
|
|
@@ -253,7 +370,9 @@ export function renderRule(learning, metadata) {
|
|
|
253
370
|
fm.push('---');
|
|
254
371
|
fm.push('auto-generated: true');
|
|
255
372
|
fm.push('alwaysApply: false');
|
|
256
|
-
|
|
373
|
+
// #1041: js-yaml-safe serialization — see `dumpYamlScalar` above. Byte-
|
|
374
|
+
// identical to the prior unquoted output for every colon-free description.
|
|
375
|
+
fm.push(`description: ${dumpYamlScalar(metadata.description)}`);
|
|
257
376
|
// OMIT the `globs:` key entirely when there are no globs (a host-class-only
|
|
258
377
|
// activation axis). An empty `globs:` line parses in the loader as
|
|
259
378
|
// `globs: []`, which short-circuits BEFORE the host-class gate runs
|
|
@@ -287,7 +406,6 @@ export function renderRule(learning, metadata) {
|
|
|
287
406
|
fm.push(`learning-key: ${metadata.learningKey}`);
|
|
288
407
|
fm.push(`confidence: ${metadata.confidence}`);
|
|
289
408
|
fm.push(`expires-at: ${metadata.expiresAt}`);
|
|
290
|
-
fm.push('---');
|
|
291
409
|
|
|
292
410
|
// ── Body (free markdown; does not affect frontmatter parsing) ────────────
|
|
293
411
|
// Both provenance values are rendered INSIDE an inline-code span below, which
|
|
@@ -306,6 +424,28 @@ export function renderRule(learning, metadata) {
|
|
|
306
424
|
})
|
|
307
425
|
: 'n/a';
|
|
308
426
|
|
|
427
|
+
// ── Evidence digest (#1101) ──────────────────────────────────────────────
|
|
428
|
+
// Rendered ONCE and reused, so the hashed bytes and the written bytes are the
|
|
429
|
+
// same string by construction rather than by two calls agreeing.
|
|
430
|
+
const evidenceBlock = renderEvidence(learning.evidence);
|
|
431
|
+
const evidenceRecordedAt =
|
|
432
|
+
typeof learning.created_at === 'string' && EVIDENCE_RECORDED_AT_RE.test(learning.created_at)
|
|
433
|
+
? learning.created_at
|
|
434
|
+
: '';
|
|
435
|
+
const evidenceDigest = computeEvidenceDigest({
|
|
436
|
+
learningKey: metadata.learningKey,
|
|
437
|
+
learningId,
|
|
438
|
+
sourceSession,
|
|
439
|
+
evidenceRecordedAt,
|
|
440
|
+
confidence: String(metadata.confidence),
|
|
441
|
+
evidenceBlock,
|
|
442
|
+
});
|
|
443
|
+
// Appended AFTER `expires-at` on purpose: every key BEFORE it keeps its byte
|
|
444
|
+
// position, so the frontmatter of an existing rule stays byte-stable and the
|
|
445
|
+
// digest is purely additive.
|
|
446
|
+
fm.push(`evidence-digest: ${evidenceDigest}`);
|
|
447
|
+
fm.push('---');
|
|
448
|
+
|
|
309
449
|
// The untrusted region (H1 + insight + Evidence) is FRAMED: the envelope
|
|
310
450
|
// states, in machine-generated text the record cannot forge, that everything
|
|
311
451
|
// inside is agent-authored data rather than an instruction to the reading
|
|
@@ -319,7 +459,7 @@ export function renderRule(learning, metadata) {
|
|
|
319
459
|
insight,
|
|
320
460
|
'',
|
|
321
461
|
'## Evidence',
|
|
322
|
-
|
|
462
|
+
evidenceBlock,
|
|
323
463
|
'',
|
|
324
464
|
UNTRUSTED_END,
|
|
325
465
|
'',
|
|
@@ -328,6 +468,11 @@ export function renderRule(learning, metadata) {
|
|
|
328
468
|
`- learning-key: \`${metadata.learningKey}\``,
|
|
329
469
|
`- learning-id: \`${learningId}\``,
|
|
330
470
|
`- source-session: \`${sourceSession}\``,
|
|
471
|
+
// #1101: the seal, its one un-derivable input, and the recipe to re-check
|
|
472
|
+
// both — everything an offline reader needs, in the file itself.
|
|
473
|
+
`- evidence-digest: \`${evidenceDigest}\``,
|
|
474
|
+
`- evidence-recorded-at: ${evidenceRecordedAt}`,
|
|
475
|
+
'- evidence-digest-input: learning-key \\n learning-id \\n source-session \\n evidence-recorded-at \\n confidence \\n <the `## Evidence` block above, verbatim UTF-8, LF-joined, no trailing newline>',
|
|
331
476
|
`- confidence: ${metadata.confidence}`,
|
|
332
477
|
'- generated-by: reconciliation-engine (Epic #693 FA2 / #695)',
|
|
333
478
|
`- expires-at: ${metadata.expiresAt}`,
|
|
@@ -43,21 +43,40 @@
|
|
|
43
43
|
* throw to an audited `emit/render error: …` rejection, never a crash.
|
|
44
44
|
*
|
|
45
45
|
* -- What this module deliberately does NOT touch -----------------------------
|
|
46
|
-
* Colons
|
|
47
|
-
*
|
|
48
|
-
* (
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
*
|
|
46
|
+
* Colons in general (a mid-line `#`), non-ASCII punctuation (`—` is content;
|
|
47
|
+
* the trailing `…` is the emitter's own truncation marker), `*`/`**` inside
|
|
48
|
+
* globs (already double-quoted by the renderer), body `---` horizontal rules
|
|
49
|
+
* (a WRAPPER-side concern in print-applicable-rules.mjs, not ours), fenced
|
|
50
|
+
* code blocks, markdown headings, inline backticks and HTML comments in the
|
|
51
|
+
* body — those ARE the reconciler's own output shape, and this module's SHAPE
|
|
52
|
+
* asserts below are unaffected either way: they reject on control characters,
|
|
53
|
+
* dangerous invisibles, wrapper-forgery literals and length, never on colons.
|
|
54
|
+
*
|
|
55
|
+
* The frontmatter parser THIS module's asserts are shaped for is HAND-ROLLED,
|
|
56
|
+
* not a YAML library (`rule-loader.mjs` imports only node:fs/path/module): a
|
|
57
|
+
* `:` in a scalar is harmless there, a mid-line `#` is harmless, quotes
|
|
58
|
+
* survive — the ONE escape is a newline, which starts a new top-level key.
|
|
59
|
+
* That is why the machine-value asserts below are shaped as "no control
|
|
60
|
+
* characters / exact token shape" and NOT as YAML quoting or colon escaping,
|
|
61
|
+
* which would be both wrong and destructive for THIS parser.
|
|
62
|
+
*
|
|
63
|
+
* That reasoning stopped being the whole story once #1041 landed: Claude
|
|
64
|
+
* Code's OWN native frontmatter loader — the thing that actually delivers a
|
|
65
|
+
* rendered rule into every future agent's context — parses the SAME file as
|
|
66
|
+
* real YAML, and a colon-bearing `description:` value emitted unquoted
|
|
67
|
+
* (`description: fixes X: this breaks Y`) is a syntax error there ("bad
|
|
68
|
+
* indentation of a mapping entry"), not merely a shape this module chose not
|
|
69
|
+
* to touch. The fix for that is NOT a new assert in this module — 14 live
|
|
70
|
+
* `description:` lines legitimately carry a second colon, and this module's
|
|
71
|
+
* job stays "reject an unsafe SHAPE", never "reformat a safe one". Instead
|
|
72
|
+
* `renderer.mjs`'s `dumpYamlScalar` (built on the already-installed `js-yaml`
|
|
73
|
+
* dependency) serializes the `description` value returned by
|
|
74
|
+
* {@link assertSafeDescription} as a real YAML scalar (plain when safe,
|
|
75
|
+
* single-quoted only when a colon or similar forces it) at the render POINT,
|
|
76
|
+
* after this module's shape gate has already run. The two layers stay
|
|
77
|
+
* distinct on purpose: this module still decides whether the value may be
|
|
78
|
+
* emitted at all; `renderer.mjs` decides how to spell the value both parsers
|
|
79
|
+
* — the hand-rolled one AND js-yaml — can read back unchanged.
|
|
61
80
|
*
|
|
62
81
|
* Pure functions — no file I/O, no process state. Part of issue #1015.
|
|
63
82
|
*
|
|
@@ -190,8 +209,12 @@ export const PROVENANCE_TOKEN_RE = /^[A-Za-z0-9._:-]+$/;
|
|
|
190
209
|
// byte gate. The property class keeps the source pure ASCII by construction.
|
|
191
210
|
//
|
|
192
211
|
// CONTROL_RE is the REJECT test for single-line machine values, where a TAB or
|
|
193
|
-
// LF is an escape rather than formatting.
|
|
194
|
-
|
|
212
|
+
// LF is an escape rather than formatting. U+2028/U+2029 (Zl/Zp) are NOT \p{Cc}
|
|
213
|
+
// but YAML 1.1 and js-yaml's loader treat them as line breaks — measured
|
|
214
|
+
// 2026-08-23 (W4-R2): `"safe\u2028alwaysApply: true"` passed this gate and was
|
|
215
|
+
// neutralised only by js-yaml's escaping in the renderer. The guarantee
|
|
216
|
+
// belongs in the gate, not in a downstream serializer's style choice.
|
|
217
|
+
const CONTROL_RE = /[\p{Cc}\u2028\u2029]/u;
|
|
195
218
|
|
|
196
219
|
// The same category, global, for the prose strip. TAB/LF are re-admitted by the
|
|
197
220
|
// replacement callback in `sanitizeProse` (multi-line prose is legitimate in a
|