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
|
@@ -11,6 +11,9 @@
|
|
|
11
11
|
* Responsibilities:
|
|
12
12
|
* - Acquire a per-write file lock (`.orchestrator/rules.lock`) to serialise
|
|
13
13
|
* concurrent writers — mirrors PSA-005 (withStateMdLock) pattern.
|
|
14
|
+
* - Before any write: project the instruction budget after the batch and
|
|
15
|
+
* refuse the WHOLE batch (nothing written, nothing stamped) when it would
|
|
16
|
+
* breach a ceiling — see `budgetPreflight`.
|
|
14
17
|
* - For each approved proposal: path-safety guard → STRUCTURAL content gate
|
|
15
18
|
* (#1015, see {@link frontmatterRefusalReason}) → mkdirSync → atomic
|
|
16
19
|
* tmp+rename write → stamp the idempotency sidecar terminal via
|
|
@@ -54,8 +57,20 @@
|
|
|
54
57
|
* @module reconcile/writer
|
|
55
58
|
*/
|
|
56
59
|
|
|
57
|
-
import {
|
|
60
|
+
import {
|
|
61
|
+
mkdirSync,
|
|
62
|
+
mkdtempSync,
|
|
63
|
+
writeFileSync,
|
|
64
|
+
readFileSync,
|
|
65
|
+
readdirSync,
|
|
66
|
+
renameSync,
|
|
67
|
+
appendFileSync,
|
|
68
|
+
realpathSync,
|
|
69
|
+
rmSync,
|
|
70
|
+
statSync,
|
|
71
|
+
} from 'node:fs';
|
|
58
72
|
import { randomBytes } from 'node:crypto';
|
|
73
|
+
import { tmpdir } from 'node:os';
|
|
59
74
|
import path from 'node:path';
|
|
60
75
|
|
|
61
76
|
import { withFileLock } from '../file-lock.mjs';
|
|
@@ -490,6 +505,228 @@ function isOperatorRejection(item) {
|
|
|
490
505
|
return typeof item.content === 'string' && item.content.length > 0;
|
|
491
506
|
}
|
|
492
507
|
|
|
508
|
+
// ---------------------------------------------------------------------------
|
|
509
|
+
// Instruction-budget pre-flight (#1316 follow-up)
|
|
510
|
+
// ---------------------------------------------------------------------------
|
|
511
|
+
|
|
512
|
+
/** Refusal reason returned when the projected corpus would breach a ceiling. */
|
|
513
|
+
export const BUDGET_REFUSAL_REASON = 'instruction-budget-exceeded';
|
|
514
|
+
|
|
515
|
+
const BUDGET_HINT =
|
|
516
|
+
'absorb the approved rule(s) into an existing thematic file under .claude/rules/ ' +
|
|
517
|
+
'(N provenance pairs in ONE file, globs:/paths: mirrored — docs/rule-authoring.md § Consolidated rules) ' +
|
|
518
|
+
'and re-run; never raise the ceiling';
|
|
519
|
+
|
|
520
|
+
/**
|
|
521
|
+
* @typedef {Object} BudgetRefusal
|
|
522
|
+
* @property {string} reason - always {@link BUDGET_REFUSAL_REASON}.
|
|
523
|
+
* @property {'path-scoped-bytes'|'generated-bytes'|'always-on-bytes'|'directives'} axis
|
|
524
|
+
* @property {number} current - the axis value before the write.
|
|
525
|
+
* @property {number} projected - the axis value after the write.
|
|
526
|
+
* @property {number} ceiling - the ceiling the projection breaches.
|
|
527
|
+
* @property {string} hint
|
|
528
|
+
*/
|
|
529
|
+
|
|
530
|
+
/**
|
|
531
|
+
* Project the instruction budget AFTER the pending repo-local writes and refuse
|
|
532
|
+
* the whole batch if the projection would breach a ceiling.
|
|
533
|
+
*
|
|
534
|
+
* WHY A PROJECTION AND NOT WRITE-THEN-ROLLBACK: a rollback has to undo rule
|
|
535
|
+
* files AND sidecar stamps, and a half-failed rollback is worse than the breach.
|
|
536
|
+
* Refusing before the first byte lands keeps the failure clean.
|
|
537
|
+
*
|
|
538
|
+
* HOW: the guard's own `computeInstructionBudget` runs twice — on the live rules
|
|
539
|
+
* directory, and on a throwaway overlay (a copy of that directory with the
|
|
540
|
+
* pending contents written over it). Re-running the guard's code rather than
|
|
541
|
+
* re-deriving its byte count means the projection cannot disagree with the
|
|
542
|
+
* verdict: same frontmatter stripping, same `globs:`/provenance predicates,
|
|
543
|
+
* all four axes, and an update of an existing file lands as its delta by
|
|
544
|
+
* construction. Ceilings follow `checkInstructionBudget`'s precedence
|
|
545
|
+
* (Session Config `instruction-budget.*` > module default), and
|
|
546
|
+
* `enabled: false` / `mode: off` skips the pre-flight — the operator opted out
|
|
547
|
+
* of the guard.
|
|
548
|
+
*
|
|
549
|
+
* REFUSE ONLY WHAT THIS WRITE WORSENS: an axis blocks the batch only when it is
|
|
550
|
+
* over in the projection AND the write grows it. A corpus that is already over
|
|
551
|
+
* therefore still accepts a shrinking consolidation — the remedy must never be
|
|
552
|
+
* blocked by the breach it repairs — and an unrelated pre-existing breach (say,
|
|
553
|
+
* the hand-authored always-on directive count) does not freeze reconcile.
|
|
554
|
+
*
|
|
555
|
+
* CEILING (BV-004): this is a projection of THIS batch against the corpus as it
|
|
556
|
+
* stands inside the rules lock. It does not cover rule edits made by hand or by
|
|
557
|
+
* any writer that does not take `.orchestrator/rules.lock`, and it projects
|
|
558
|
+
* with the Session Config ceilings while `tests/rules/receiving-review.test.mjs`
|
|
559
|
+
* asserts against the module defaults — so a repo that RAISES its ceiling in
|
|
560
|
+
* config can pass here and still go red there; that live test stays the second
|
|
561
|
+
* safety net. It also fails OPEN (one `errors[]` warning, then the write
|
|
562
|
+
* proceeds) when the guard cannot load or the overlay cannot be built,
|
|
563
|
+
* because blocking every reconcile write on a broken measurement is the larger
|
|
564
|
+
* harm. Revisit if a second writer of `.claude/rules/` appears, or if a repo
|
|
565
|
+
* ever carries a config ceiling above the module default.
|
|
566
|
+
*
|
|
567
|
+
* @param {Object} ctx
|
|
568
|
+
* @param {WriterApprovedItem[]} ctx.approvedItems
|
|
569
|
+
* @param {PreparedTarget|undefined} ctx.prep - the prepared `repo-local` target.
|
|
570
|
+
* @param {{repoRoot?: string, baselineRoot?: string}} ctx.roots
|
|
571
|
+
* @param {string} ctx.repoRoot
|
|
572
|
+
* @param {string[]} ctx.errors - receives fail-open warnings.
|
|
573
|
+
* @returns {Promise<BudgetRefusal|null>} null ⇒ proceed with the writes.
|
|
574
|
+
*/
|
|
575
|
+
async function budgetPreflight({ approvedItems, prep, roots, repoRoot, errors }) {
|
|
576
|
+
if (!prep || !prep.ok) return null;
|
|
577
|
+
|
|
578
|
+
// Only what the write loop below would actually put on disk counts: the same
|
|
579
|
+
// content-type check, structural gate and destination resolution. Errors from
|
|
580
|
+
// this dry resolution go to a throwaway sink — the write loop reports them.
|
|
581
|
+
/** @type {Array<{rel: string, content: string}>} */
|
|
582
|
+
const pending = [];
|
|
583
|
+
for (const item of approvedItems) {
|
|
584
|
+
if (!item || typeof item.content !== 'string') continue;
|
|
585
|
+
if (frontmatterRefusalReason(item.content) !== null) continue;
|
|
586
|
+
const dest = resolveDest(item, 'repo-local', prep, roots, []);
|
|
587
|
+
if (dest === null) continue;
|
|
588
|
+
pending.push({ rel: path.relative(prep.dir, dest), content: item.content });
|
|
589
|
+
}
|
|
590
|
+
if (pending.length === 0) return null;
|
|
591
|
+
|
|
592
|
+
let overlay = null;
|
|
593
|
+
try {
|
|
594
|
+
// Dynamic import keeps the guard out of this module's static graph.
|
|
595
|
+
const guard = await import('../instruction-budget-guard.mjs');
|
|
596
|
+
const cfg = guard.loadInstructionBudgetConfig(repoRoot);
|
|
597
|
+
if (!cfg.enabled || cfg.mode === 'off') return null;
|
|
598
|
+
// computeInstructionBudget falls back to its module default for any
|
|
599
|
+
// non-number, so an absent optional key needs no special case here.
|
|
600
|
+
const ceilings = {
|
|
601
|
+
ceiling: cfg.ceiling,
|
|
602
|
+
byteCeiling: cfg['byte-ceiling'],
|
|
603
|
+
generatedByteCeiling: cfg['generated-byte-ceiling'],
|
|
604
|
+
pathScopedByteCeiling: cfg['path-scoped-byte-ceiling'],
|
|
605
|
+
};
|
|
606
|
+
|
|
607
|
+
const current = guard.computeInstructionBudget({ repoRoot, rulesDir: prep.dir, ...ceilings });
|
|
608
|
+
|
|
609
|
+
overlay = mkdtempSync(path.join(tmpdir(), 'reconcile-budget-'));
|
|
610
|
+
// The guard reads the rules directory flat (`readdirSync`, `.md` only).
|
|
611
|
+
for (const name of readdirSync(prep.dir)) {
|
|
612
|
+
if (!name.endsWith('.md')) continue;
|
|
613
|
+
try {
|
|
614
|
+
writeFileSync(path.join(overlay, name), readFileSync(path.join(prep.dir, name), 'utf8'), 'utf8');
|
|
615
|
+
} catch {
|
|
616
|
+
/* unreadable or a directory — the guard skips it too */
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
for (const { rel, content } of pending) {
|
|
620
|
+
const dest = path.join(overlay, rel);
|
|
621
|
+
mkdirSync(path.dirname(dest), { recursive: true });
|
|
622
|
+
writeFileSync(dest, content, 'utf8');
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
const projected = guard.computeInstructionBudget({ repoRoot, rulesDir: overlay, ...ceilings });
|
|
626
|
+
|
|
627
|
+
const axes = [
|
|
628
|
+
['path-scoped-bytes', current.bySurface.pathScoped.bytes, projected.bySurface.pathScoped.bytes, projected.pathScopedByteCeiling, projected.overPathScopedBudget],
|
|
629
|
+
['generated-bytes', current.bySurface.generated.bytes, projected.bySurface.generated.bytes, projected.generatedByteCeiling, projected.overGeneratedBudget],
|
|
630
|
+
['always-on-bytes', current.totalBytes, projected.totalBytes, projected.byteCeiling, projected.overByteBudget],
|
|
631
|
+
['directives', current.totalDirectives, projected.totalDirectives, projected.ceiling, projected.overDirectiveBudget],
|
|
632
|
+
];
|
|
633
|
+
for (const [axis, cur, proj, ceiling, over] of axes) {
|
|
634
|
+
if (over && proj > cur) {
|
|
635
|
+
return { reason: BUDGET_REFUSAL_REASON, axis, current: cur, projected: proj, ceiling, hint: BUDGET_HINT };
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
return null;
|
|
639
|
+
} catch (err) {
|
|
640
|
+
const msg = err && err.message ? err.message : String(err);
|
|
641
|
+
errors.push(`instruction-budget pre-flight could not run (${msg}) — writes proceed unchecked (fail-open)`);
|
|
642
|
+
return null;
|
|
643
|
+
} finally {
|
|
644
|
+
if (overlay !== null) {
|
|
645
|
+
try {
|
|
646
|
+
rmSync(overlay, { recursive: true, force: true });
|
|
647
|
+
} catch {
|
|
648
|
+
/* a leftover tmp dir is harmless */
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
// ---------------------------------------------------------------------------
|
|
655
|
+
// Telemetry (#1307)
|
|
656
|
+
// ---------------------------------------------------------------------------
|
|
657
|
+
|
|
658
|
+
/**
|
|
659
|
+
* Ledger name of the rule-write event (issue #1307).
|
|
660
|
+
*
|
|
661
|
+
* WHY A SECOND EVENT AND NOT A FIELD ON `orchestrator.reconcile.completed`:
|
|
662
|
+
* that event is emitted by the `runReconcile` wrapper — BEFORE the operator
|
|
663
|
+
* approval AUQ and before this module runs at all. A `dry_run: false` record
|
|
664
|
+
* therefore proves only that the engine ran and merged the candidate store; an
|
|
665
|
+
* operator who declined every proposal emits a byte-identical record to one who
|
|
666
|
+
* approved five. The emitter has to sit where the thing it proves happens, and
|
|
667
|
+
* that is here: after the write pass, in the one module that writes rule files.
|
|
668
|
+
*
|
|
669
|
+
* Emitting is NOT writing — the #693 FA2/FA3 brandmauer is untouched. This
|
|
670
|
+
* module remains the only writer of `.claude/rules/`, and every write it makes
|
|
671
|
+
* is still an operator-approved item handed in via `approved`.
|
|
672
|
+
*/
|
|
673
|
+
export const RULES_WRITTEN_EVENT = 'orchestrator.reconcile.rules_written';
|
|
674
|
+
|
|
675
|
+
/**
|
|
676
|
+
* Record ONE rule-write pass in the repo's event ledger — best-effort.
|
|
677
|
+
*
|
|
678
|
+
* ZERO-WRITE IS EMITTED, DELIBERATELY. The alternative (emit only on a
|
|
679
|
+
* successful write) makes the maintenance loop unable to separate three
|
|
680
|
+
* materially different outcomes that would all produce NO record: the operator
|
|
681
|
+
* declined every proposal, every write was REFUSED by a guard (path-safety,
|
|
682
|
+
* the #1015 content gate, a missing baseline root), and the writer was never
|
|
683
|
+
* reached at all. `.claude/rules/host-resources.md` § HR-105 is explicit that a
|
|
684
|
+
* rule whose firing you cannot falsify is not a rule, and the sibling event's
|
|
685
|
+
* own docs row states the same convention in one line: *"`dry_run` is the
|
|
686
|
+
* discriminator, not the event's absence"*. So the discriminator here is a
|
|
687
|
+
* FIELD — `rules_written` against `approved_proposals` — never the absence of a
|
|
688
|
+
* record. The one case that emits nothing is the caller's true no-op (neither
|
|
689
|
+
* an approved nor a rejected item), which returns before the lock is taken.
|
|
690
|
+
*
|
|
691
|
+
* NEVER THROWS, and never propagates: `emitEvent` throws `EventValidationError`
|
|
692
|
+
* on a malformed record, and `writeApprovedRules`'s never-throws contract is
|
|
693
|
+
* older and more load-bearing than this telemetry. A failed emit degrades to a
|
|
694
|
+
* stderr WARN and the write result is returned unchanged.
|
|
695
|
+
*
|
|
696
|
+
* @param {WriteApprovedRulesResult} result
|
|
697
|
+
* @param {{repoRoot?: string, targets: string[], approvedCount: number}} ctx
|
|
698
|
+
* @returns {Promise<void>}
|
|
699
|
+
*/
|
|
700
|
+
async function emitRulesWritten(result, ctx) {
|
|
701
|
+
const { repoRoot } = ctx;
|
|
702
|
+
// Same refusal as `emitReconcileCompleted` (#1119): with no repoRoot the
|
|
703
|
+
// ambient `SO_PROJECT_DIR` would receive synthetic records on every test run.
|
|
704
|
+
if (typeof repoRoot !== 'string' || repoRoot.trim() === '') return;
|
|
705
|
+
|
|
706
|
+
/** @type {Record<string, unknown>} */
|
|
707
|
+
const payload = {
|
|
708
|
+
rules_written: result.written,
|
|
709
|
+
approved_proposals: ctx.approvedCount,
|
|
710
|
+
rejected_archived: result.archived,
|
|
711
|
+
write_errors: result.errors.length,
|
|
712
|
+
};
|
|
713
|
+
// Allowlisted to the CLOSED {@link TARGET_DIRS} key set before entering the
|
|
714
|
+
// ledger — `targets` originates in operator-authored Session Config and is
|
|
715
|
+
// unbounded there, so an unknown value is untrusted text, never a
|
|
716
|
+
// measurement. Same treatment as `buildReconcilePayload`'s `targets`.
|
|
717
|
+
const known = Object.keys(TARGET_DIRS);
|
|
718
|
+
const targets = ctx.targets.filter((t) => known.includes(t));
|
|
719
|
+
if (targets.length > 0) payload.targets = targets;
|
|
720
|
+
|
|
721
|
+
try {
|
|
722
|
+
const { emitEvent } = await import('../events.mjs');
|
|
723
|
+
await emitEvent(RULES_WRITTEN_EVENT, payload, { repoRoot });
|
|
724
|
+
} catch (err) {
|
|
725
|
+
const msg = err && err.message ? err.message : String(err);
|
|
726
|
+
process.stderr.write(`reconcile-writer: ${RULES_WRITTEN_EVENT} emit failed (non-fatal): ${msg}\n`);
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
|
|
493
730
|
// ---------------------------------------------------------------------------
|
|
494
731
|
// Public API
|
|
495
732
|
// ---------------------------------------------------------------------------
|
|
@@ -532,6 +769,12 @@ function isOperatorRejection(item) {
|
|
|
532
769
|
* @property {number} written - number of rule files successfully written.
|
|
533
770
|
* @property {number} archived - number of rejected records appended to the log.
|
|
534
771
|
* @property {string[]} errors - per-item error strings (never fatal).
|
|
772
|
+
*
|
|
773
|
+
* On an instruction-budget refusal (see `budgetPreflight`) the same object also
|
|
774
|
+
* carries `ok: false` plus every {@link BudgetRefusal} field, `written` and
|
|
775
|
+
* `archived` are 0, and `errors[]` holds one line naming the axis and the hint —
|
|
776
|
+
* so a caller that only surfaces `errors[]` still shows the refusal. The success
|
|
777
|
+
* shape carries no `ok` key.
|
|
535
778
|
*/
|
|
536
779
|
|
|
537
780
|
/**
|
|
@@ -618,6 +861,24 @@ export async function writeApprovedRules({
|
|
|
618
861
|
}
|
|
619
862
|
}
|
|
620
863
|
|
|
864
|
+
// Instruction-budget pre-flight — before the first write, inside the
|
|
865
|
+
// lock so no concurrent reconcile writer moves the corpus between the
|
|
866
|
+
// projection and the write. A refusal writes NOTHING: no rule file, no
|
|
867
|
+
// rejected-log line, no sidecar stamp (approved or rejected).
|
|
868
|
+
const budgetRefusal = await budgetPreflight({
|
|
869
|
+
approvedItems,
|
|
870
|
+
prep: prepared.get('repo-local'),
|
|
871
|
+
roots,
|
|
872
|
+
repoRoot,
|
|
873
|
+
errors,
|
|
874
|
+
});
|
|
875
|
+
if (budgetRefusal !== null) {
|
|
876
|
+
errors.push(
|
|
877
|
+
`${budgetRefusal.reason}: ${budgetRefusal.axis} would go ${budgetRefusal.current} → ${budgetRefusal.projected} (ceiling ${budgetRefusal.ceiling}) — nothing written, no candidate marked processed; ${budgetRefusal.hint}`,
|
|
878
|
+
);
|
|
879
|
+
return { written: 0, archived: 0, errors, ok: false, ...budgetRefusal };
|
|
880
|
+
}
|
|
881
|
+
|
|
621
882
|
// ── Step 1: write approved rule files ──────────────────────────────────
|
|
622
883
|
for (const item of approvedItems) {
|
|
623
884
|
// Item-level guards run ONCE, before any target loop: content is a
|
|
@@ -765,15 +1026,21 @@ export async function writeApprovedRules({
|
|
|
765
1026
|
{ timeoutMs: 10000 },
|
|
766
1027
|
);
|
|
767
1028
|
|
|
768
|
-
//
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
1029
|
+
// One emit for BOTH return points (#1307). A lock-acquisition failure is a
|
|
1030
|
+
// zero-write pass like any other and is recorded as one — its `write_errors: 1`
|
|
1031
|
+
// plus a non-zero `approved_proposals` is exactly the shape an operator needs
|
|
1032
|
+
// to see, and an emit placed on the success path alone would drop it silently.
|
|
1033
|
+
const result =
|
|
1034
|
+
lockResult.ok === false
|
|
1035
|
+
? { written: 0, archived: 0, errors: [`lock-${lockResult.reason ?? 'unknown'}`] }
|
|
1036
|
+
: // Unwrap the result returned from inside the lock body.
|
|
1037
|
+
lockResult.value;
|
|
1038
|
+
|
|
1039
|
+
await emitRulesWritten(result, {
|
|
1040
|
+
repoRoot,
|
|
1041
|
+
targets: effectiveTargets,
|
|
1042
|
+
approvedCount: approvedItems.length,
|
|
1043
|
+
});
|
|
776
1044
|
|
|
777
|
-
|
|
778
|
-
return lockResult.value;
|
|
1045
|
+
return result;
|
|
779
1046
|
}
|
|
@@ -61,6 +61,14 @@
|
|
|
61
61
|
* of starts is a broken instrument to be re-aimed, not a policy to obey.
|
|
62
62
|
*/
|
|
63
63
|
|
|
64
|
+
// `session-shape.mjs` is the ONE exported `agents-per-wave` resolver; this
|
|
65
|
+
// module used to carry a private third copy. Import-graph note (this file is
|
|
66
|
+
// hook-reachable via `hooks/_lib/hook-import-set.json`): `session-shape.mjs`
|
|
67
|
+
// statically imports only `session-schema/constants.mjs`, a pure-data leaf, and
|
|
68
|
+
// defers `events.mjs` to a lazy `await import()` — so nothing runs at module
|
|
69
|
+
// load here that did not run before.
|
|
70
|
+
import { resolveAgentCap } from '../session-shape.mjs';
|
|
71
|
+
|
|
64
72
|
// ---------------------------------------------------------------------------
|
|
65
73
|
// Verdict precedence helper
|
|
66
74
|
// ---------------------------------------------------------------------------
|
|
@@ -252,29 +260,19 @@ function concurrencySignal(snapshot, concWarn) {
|
|
|
252
260
|
}
|
|
253
261
|
|
|
254
262
|
/**
|
|
255
|
-
*
|
|
256
|
-
* when no cap should apply. Mirrors `resolveApwCap()` in
|
|
257
|
-
* `../wave-resource-gate.mjs` — kept as a local pure helper here rather than
|
|
258
|
-
* a cross-module import since both sites are ≤10 lines and evolve
|
|
259
|
-
* independently per their own gate's options shape.
|
|
263
|
+
* The session type this module resolves `agents-per-wave` FOR: none.
|
|
260
264
|
*
|
|
261
|
-
* `evaluate()` has no session-mode input in scope, so
|
|
262
|
-
*
|
|
263
|
-
* override as `<default> (mode: <higher-ceiling>)`, i.e.
|
|
264
|
-
* MORE restrictive of the pair
|
|
265
|
-
* mode-specific ceiling,
|
|
265
|
+
* `evaluate()` has no session-mode input in scope, so it deliberately asks
|
|
266
|
+
* `resolveAgentCap` for no type and takes the `.default` fallback. The HR-003
|
|
267
|
+
* convention writes the override as `<default> (mode: <higher-ceiling>)`, i.e.
|
|
268
|
+
* `default` is the MORE restrictive of the pair — under-applying a looser
|
|
269
|
+
* mode-specific ceiling is safe, while passing a guessed `'deep'` here would
|
|
270
|
+
* silently RAISE a heavy repo's ceiling to the deep override on a loaded host.
|
|
271
|
+
* Naming the absence is what keeps that from being written by accident.
|
|
266
272
|
*
|
|
267
|
-
* @
|
|
268
|
-
* @returns {number|null}
|
|
273
|
+
* @type {undefined}
|
|
269
274
|
*/
|
|
270
|
-
|
|
271
|
-
if (typeof cap === 'number') return Number.isFinite(cap) ? cap : null;
|
|
272
|
-
if (cap !== null && typeof cap === 'object' && !Array.isArray(cap)) {
|
|
273
|
-
const def = cap.default;
|
|
274
|
-
return typeof def === 'number' && Number.isFinite(def) ? def : null;
|
|
275
|
-
}
|
|
276
|
-
return null;
|
|
277
|
-
}
|
|
275
|
+
const MODE_BLIND_SESSION_TYPE = undefined;
|
|
278
276
|
|
|
279
277
|
/**
|
|
280
278
|
* Evaluate a snapshot against `resource-thresholds` (from Session Config #166)
|
|
@@ -423,7 +421,7 @@ export function evaluate(snapshot, thresholds, options = {}) {
|
|
|
423
421
|
// the live-probe signals already computed.
|
|
424
422
|
// ---------------------------------------------------------------------------
|
|
425
423
|
const { heavyRepo, agentsPerWave } = options;
|
|
426
|
-
const resolvedApwCap =
|
|
424
|
+
const resolvedApwCap = resolveAgentCap(agentsPerWave, MODE_BLIND_SESSION_TYPE);
|
|
427
425
|
if (heavyRepo === true && resolvedApwCap !== null) {
|
|
428
426
|
cap = cap === null ? resolvedApwCap : Math.min(cap, resolvedApwCap);
|
|
429
427
|
}
|
|
@@ -121,7 +121,7 @@ function listManifestCategories(indexContent) {
|
|
|
121
121
|
* @param {string|null} explicitArchetype
|
|
122
122
|
* @returns {{archetype: string|null, known: boolean}}
|
|
123
123
|
*/
|
|
124
|
-
function resolveArchetype(repoRoot, explicitArchetype) {
|
|
124
|
+
export function resolveArchetype(repoRoot, explicitArchetype) {
|
|
125
125
|
if (explicitArchetype) {
|
|
126
126
|
return { archetype: explicitArchetype.trim().toLowerCase(), known: true };
|
|
127
127
|
}
|
|
@@ -194,6 +194,12 @@ function escapeRegex(s) {
|
|
|
194
194
|
* at least one concrete bullet entry. This keeps `/bootstrap --sync-rules`
|
|
195
195
|
* ready for future opt-in categories without requiring CLI changes.
|
|
196
196
|
*
|
|
197
|
+
* `requiredBasenames` optionally adds explicitly required manifest entries,
|
|
198
|
+
* regardless of category or archetype scope. The caller supplies this data;
|
|
199
|
+
* this synchronous writer performs no external lookup. Every requested name
|
|
200
|
+
* must resolve uniquely in the full manifest before ANY file is written.
|
|
201
|
+
* Source validation, provenance, local overrides and dry-run still apply.
|
|
202
|
+
*
|
|
197
203
|
* Vendoring sanitizer (issue #1098): every source file that reaches the write
|
|
198
204
|
* decision is additionally scanned by `scanVendoringLeaks()`, and its findings
|
|
199
205
|
* are collected into the additive `sanitizer[]` array. This runs in
|
|
@@ -214,7 +220,8 @@ function escapeRegex(s) {
|
|
|
214
220
|
* dryRun?: boolean,
|
|
215
221
|
* validate?: boolean,
|
|
216
222
|
* requireProvenance?: boolean,
|
|
217
|
-
* archetype?: string|null
|
|
223
|
+
* archetype?: string|null,
|
|
224
|
+
* requiredBasenames?: string[]|null
|
|
218
225
|
* }} opts
|
|
219
226
|
* @returns {{
|
|
220
227
|
* written: string[],
|
|
@@ -233,6 +240,7 @@ export function syncRules({
|
|
|
233
240
|
validate = true,
|
|
234
241
|
requireProvenance = true,
|
|
235
242
|
archetype = null,
|
|
243
|
+
requiredBasenames = null,
|
|
236
244
|
} = {}) {
|
|
237
245
|
const written = [];
|
|
238
246
|
const skipped = [];
|
|
@@ -268,13 +276,35 @@ export function syncRules({
|
|
|
268
276
|
? categories
|
|
269
277
|
: listManifestCategories(indexContent);
|
|
270
278
|
const entries = parseIndex(indexContent, selectedCategories);
|
|
279
|
+
const fullManifest = parseIndex(indexContent, listManifestCategories(indexContent));
|
|
280
|
+
const required = new Set();
|
|
281
|
+
if (requiredBasenames !== null) {
|
|
282
|
+
if (!Array.isArray(requiredBasenames)) {
|
|
283
|
+
errors.push({ file: '_index.md', reason: 'requiredBasenames must be an array of unique manifest basenames' });
|
|
284
|
+
} else {
|
|
285
|
+
for (const name of requiredBasenames) {
|
|
286
|
+
if (typeof name !== 'string' || !/^[A-Za-z0-9][A-Za-z0-9_.-]*\.md$/.test(name) || required.has(name)) {
|
|
287
|
+
errors.push({ file: '_index.md', reason: 'invalid or duplicate required basename' });
|
|
288
|
+
continue;
|
|
289
|
+
}
|
|
290
|
+
required.add(name);
|
|
291
|
+
const matching = fullManifest.filter((entry) => basename(entry.relPath) === name);
|
|
292
|
+
if (matching.length !== 1) {
|
|
293
|
+
errors.push({ file: name, reason: 'required basename must resolve uniquely in _index.md' });
|
|
294
|
+
} else if (!entries.some((entry) => entry.relPath === matching[0].relPath)) {
|
|
295
|
+
entries.push(matching[0]);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
if (errors.length > 0) return { written, skipped, preserved, errors, warnings, sanitizer };
|
|
300
|
+
}
|
|
271
301
|
|
|
272
302
|
// Resolvability for the See-Also sanitizer is judged against the FULL
|
|
273
303
|
// manifest, not `selectedCategories`: an archetype-scoped rule is a
|
|
274
304
|
// legitimate citation target in every repo whose archetype matches, so
|
|
275
305
|
// narrowing this to the current selection would report false leaks.
|
|
276
306
|
const manifestBasenames = new Set(
|
|
277
|
-
|
|
307
|
+
fullManifest.map((e) => basename(e.relPath)),
|
|
278
308
|
);
|
|
279
309
|
|
|
280
310
|
if (entries.length === 0) {
|
|
@@ -297,7 +327,7 @@ export function syncRules({
|
|
|
297
327
|
|
|
298
328
|
// Archetype filter (issue #722 Epic A Wave 3) — evaluated before any
|
|
299
329
|
// file IO, so a skip never triggers a spurious "source file not found".
|
|
300
|
-
if (archetypes !== null) {
|
|
330
|
+
if (archetypes !== null && !required.has(basename(relPath))) {
|
|
301
331
|
if (!resolvedArchetype.known) {
|
|
302
332
|
skipped.push({ file: relPath, reason: 'archetype-unknown' });
|
|
303
333
|
continue;
|