session-orchestrator 5.2.0 → 5.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.agents/skills/architecture/SKILL.md +3 -1
- package/.agents/skills/autopilot/SKILL.md +5 -1
- package/.agents/skills/autopilot/agents/openai.yaml +5 -0
- package/.agents/skills/bootstrap/SKILL.md +5 -1
- package/.agents/skills/bootstrap/agents/openai.yaml +5 -0
- package/.agents/skills/brainstorm/SKILL.md +5 -1
- package/.agents/skills/brainstorm/agents/openai.yaml +5 -0
- package/.agents/skills/claude-md-drift-check/SKILL.md +3 -1
- package/.agents/skills/close/SKILL.md +5 -1
- package/.agents/skills/close/agents/openai.yaml +5 -0
- package/.agents/skills/convergence-monitoring/SKILL.md +4 -2
- package/.agents/skills/debug/SKILL.md +5 -1
- package/.agents/skills/debug/agents/openai.yaml +5 -0
- package/.agents/skills/discovery/SKILL.md +5 -1
- package/.agents/skills/discovery/agents/openai.yaml +5 -0
- package/.agents/skills/dispatcher/SKILL.md +5 -1
- package/.agents/skills/dispatcher/agents/openai.yaml +5 -0
- package/.agents/skills/docs-orchestrator/SKILL.md +3 -1
- package/.agents/skills/ecosystem-health/SKILL.md +3 -1
- package/.agents/skills/eli5/SKILL.md +5 -1
- package/.agents/skills/eli5/agents/openai.yaml +5 -0
- package/.agents/skills/eval/SKILL.md +6 -2
- package/.agents/skills/eval/agents/openai.yaml +5 -0
- package/.agents/skills/evolve/SKILL.md +6 -2
- package/.agents/skills/evolve/agents/openai.yaml +5 -0
- package/.agents/skills/frontmatter-guard/SKILL.md +3 -1
- package/.agents/skills/gitlab-ops/SKILL.md +3 -1
- package/.agents/skills/gitlab-portfolio/SKILL.md +3 -1
- package/.agents/skills/go/SKILL.md +5 -1
- package/.agents/skills/go/agents/openai.yaml +5 -0
- package/.agents/skills/grill/SKILL.md +5 -1
- package/.agents/skills/grill/agents/openai.yaml +5 -0
- package/.agents/skills/harness-audit/SKILL.md +5 -1
- package/.agents/skills/harness-audit/agents/openai.yaml +5 -0
- package/.agents/skills/hook-development/SKILL.md +3 -1
- package/.agents/skills/mcp-builder/SKILL.md +3 -1
- package/.agents/skills/memory-cleanup/SKILL.md +5 -1
- package/.agents/skills/memory-cleanup/agents/openai.yaml +5 -0
- package/.agents/skills/mode-selector/SKILL.md +3 -1
- package/.agents/skills/npm-publish/SKILL.md +4 -2
- package/.agents/skills/peekaboo-driver/SKILL.md +3 -1
- package/.agents/skills/persona-panel/SKILL.md +5 -1
- package/.agents/skills/persona-panel/agents/openai.yaml +5 -0
- package/.agents/skills/plan/SKILL.md +5 -1
- package/.agents/skills/plan/agents/openai.yaml +5 -0
- package/.agents/skills/playwright-driver/SKILL.md +3 -1
- package/.agents/skills/portfolio/SKILL.md +5 -1
- package/.agents/skills/portfolio/agents/openai.yaml +5 -0
- package/.agents/skills/quality-gates/SKILL.md +3 -1
- package/.agents/skills/reconcile/SKILL.md +5 -1
- package/.agents/skills/reconcile/agents/openai.yaml +5 -0
- package/.agents/skills/release/SKILL.md +5 -1
- package/.agents/skills/release/agents/openai.yaml +5 -0
- package/.agents/skills/remote-offload/SKILL.md +3 -1
- package/.agents/skills/repo-audit/SKILL.md +5 -1
- package/.agents/skills/repo-audit/agents/openai.yaml +5 -0
- package/.agents/skills/session/SKILL.md +21 -0
- package/.agents/skills/session/agents/openai.yaml +5 -0
- package/.agents/skills/session-end/SKILL.md +3 -1
- package/.agents/skills/session-plan/SKILL.md +3 -1
- package/.agents/skills/session-start/SKILL.md +3 -1
- package/.agents/skills/spinout/SKILL.md +5 -1
- package/.agents/skills/spinout/agents/openai.yaml +5 -0
- package/.agents/skills/sunset-review/SKILL.md +5 -1
- package/.agents/skills/sunset-review/agents/openai.yaml +5 -0
- package/.agents/skills/templates-ack/SKILL.md +21 -0
- package/.agents/skills/templates-ack/agents/openai.yaml +5 -0
- package/.agents/skills/test/SKILL.md +5 -1
- package/.agents/skills/test/agents/openai.yaml +5 -0
- package/.agents/skills/test-runner/SKILL.md +3 -1
- package/.agents/skills/tmux-layout/SKILL.md +3 -1
- package/.agents/skills/using-orchestrator/SKILL.md +3 -1
- package/.agents/skills/ux-grill/SKILL.md +5 -1
- package/.agents/skills/ux-grill/agents/openai.yaml +5 -0
- package/.agents/skills/vault-mirror/SKILL.md +3 -1
- package/.agents/skills/vault-sync/SKILL.md +3 -1
- package/.agents/skills/wave-executor/SKILL.md +3 -1
- package/.agents/skills/write-executable-plan/SKILL.md +3 -1
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +4 -4
- package/.codex-plugin/skills/convergence-monitoring/SKILL.md +1 -3
- package/.codex-plugin/skills/eval/SKILL.md +1 -1
- package/.codex-plugin/skills/evolve/SKILL.md +1 -1
- package/.codex-plugin/skills/npm-publish/SKILL.md +1 -3
- package/.codex-plugin/skills/session/SKILL.md +1 -1
- package/.cursor/commands/eval.md +1 -1
- package/.cursor/commands/session.md +1 -1
- package/.cursor/rules/000-session-orchestrator.mdc +0 -2
- package/.cursor/rules/050-plan.mdc +1 -1
- package/.cursor/skills/convergence-monitoring/SKILL.md +1 -0
- package/.cursor/skills/eval/SKILL.md +1 -1
- package/.cursor/skills/npm-publish/SKILL.md +1 -0
- package/.cursor-plugin/plugin.json +1 -1
- package/.orchestrator/policy/blocked-commands.json +12 -3
- package/AGENTS.md +3 -2
- package/CHANGELOG.md +136 -0
- package/README.md +9 -9
- package/SECURITY.md +12 -0
- package/agents/dialectic-deriver.md +13 -10
- package/agents/eval-judge.md +67 -45
- package/agents/skill-applied-judge.md +34 -19
- package/commands/session.md +7 -3
- package/docs/baseline.md +12 -6
- package/docs/codex-setup.md +14 -2
- package/docs/components.md +7 -5
- package/docs/events-schema.md +56 -9
- package/docs/rule-authoring.md +58 -6
- package/docs/session-config-reference.md +100 -7
- package/docs/session-config-template.md +31 -2
- package/docs/telemetry.md +2 -0
- package/hooks/_lib/hook-import-set.json +85 -8
- package/hooks/_lib/subagent-transcript.mjs +582 -31
- package/hooks/config-protection.mjs +11 -3
- package/hooks/cwd-change-restore.mjs +11 -3
- package/hooks/enforce-commands.mjs +70 -23
- package/hooks/enforce-scope.mjs +143 -33
- package/hooks/hooks-codex.json +1 -1
- package/hooks/hooks.json +1 -1
- package/hooks/loop-guard.mjs +11 -3
- package/hooks/on-session-end.mjs +58 -23
- package/hooks/on-session-start.mjs +48 -11
- package/hooks/on-stop.mjs +168 -22
- package/hooks/operator-steer.mjs +11 -3
- package/hooks/post-bash-issue-budget-refund.mjs +18 -8
- package/hooks/post-bash-write-verify.mjs +3 -2
- package/hooks/post-edit-import-probe.mjs +17 -9
- package/hooks/post-edit-validate.mjs +13 -5
- package/hooks/post-subagent-discovery-validator.mjs +98 -13
- package/hooks/post-tool-batch-wave-signal.mjs +200 -38
- package/hooks/post-tool-failure-corrective-context.mjs +11 -5
- package/hooks/post-tooluse-frontend-slop.mjs +10 -4
- package/hooks/pre-auq-clarity.mjs +15 -2
- package/hooks/pre-bash-destructive-guard.mjs +80 -9
- package/hooks/pre-bash-issue-budget.mjs +16 -11
- package/hooks/pre-bash-memory-propose-audit.mjs +86 -54
- package/hooks/pre-bash-sessions-ledger-guard.mjs +391 -20
- package/hooks/pre-bash-staging-fence.mjs +335 -31
- package/hooks/pre-bash-templates-first.mjs +19 -14
- package/hooks/pre-task-scope-disjoint.mjs +233 -2
- package/hooks/subagent-telemetry.mjs +15 -19
- package/hooks/wave-scope-commit-guard.mjs +197 -100
- package/monitors/monitors.json +1 -1
- package/output-styles/wave-summary.md +1 -1
- package/package.json +1 -1
- package/pi/prompts/eval.md +1 -1
- package/pi/prompts/session.md +1 -1
- package/rules/README.md +1 -1
- package/rules/opt-in-domain/prompt-caching.md +1 -1
- package/rules/opt-in-stack/backend-data.md +1 -1
- package/rules/opt-in-stack/backend.md +3 -3
- package/rules/opt-in-stack/frontend.md +1 -1
- package/rules/opt-in-stack/security-web.md +3 -3
- package/rules/opt-in-stack/swift.md +1 -1
- package/scripts/autopilot.mjs +23 -2
- package/scripts/backfill-abandoned-sessions.mjs +117 -15
- package/scripts/check-sessions-integrity.mjs +300 -0
- package/scripts/dialectic-deriver.mjs +50 -13
- package/scripts/emit-session.mjs +75 -29
- package/scripts/eval-session.mjs +65 -3
- package/scripts/generate-agents-skills.mjs +102 -29
- package/scripts/generate-cursor-adapter.mjs +61 -16
- package/scripts/lib/agent-status.mjs +2 -31
- package/scripts/lib/auq/clarity.mjs +10 -2
- package/scripts/lib/auq/parse.mjs +12 -31
- package/scripts/lib/auq/schema.mjs +56 -41
- package/scripts/lib/auto-dialectic.mjs +304 -15
- package/scripts/lib/autopilot/flags.mjs +12 -1
- package/scripts/lib/autopilot/kill-switches.mjs +6 -3
- package/scripts/lib/autopilot/loop.mjs +14 -1
- package/scripts/lib/autopilot/stall-sampler.mjs +80 -23
- package/scripts/lib/ci-status-banner.mjs +376 -16
- package/scripts/lib/command-blocker.mjs +275 -28
- package/scripts/lib/config/dialectic.mjs +12 -3
- package/scripts/lib/config/gate.mjs +74 -0
- package/scripts/lib/config/reaper.mjs +162 -0
- package/scripts/lib/config.mjs +14 -0
- package/scripts/lib/convergence-monitor.mjs +74 -11
- package/scripts/lib/ecosystem-health.mjs +11 -0
- package/scripts/lib/eval/engine.mjs +421 -53
- package/scripts/lib/eval/judge.mjs +463 -40
- package/scripts/lib/eval/schema.mjs +10 -1
- package/scripts/lib/events-rotation.mjs +221 -25
- package/scripts/lib/events-schema.mjs +114 -0
- package/scripts/lib/events.mjs +524 -5
- package/scripts/lib/frontmatter-guard.mjs +21 -10
- package/scripts/lib/gates/gate-baseline.mjs +27 -2
- package/scripts/lib/gates/gate-full.mjs +28 -3
- package/scripts/lib/gates/gate-helpers.mjs +243 -21
- package/scripts/lib/gates/gate-incremental.mjs +28 -3
- package/scripts/lib/gates/gate-per-file.mjs +27 -2
- package/scripts/lib/gitlab-portfolio/markdown-writer.mjs +6 -1
- package/scripts/lib/instruction-budget-guard.mjs +146 -4
- package/scripts/lib/io.mjs +42 -8
- package/scripts/lib/issue-close-strip-labels.mjs +207 -49
- package/scripts/lib/js-mask.mjs +197 -0
- package/scripts/lib/learnings/evolve-telemetry.mjs +11 -7
- package/scripts/lib/maintenance-due-banner.mjs +53 -88
- package/scripts/lib/orphan-reaper.mjs +1588 -0
- package/scripts/lib/peer-cards/merger.mjs +48 -10
- package/scripts/lib/peer-cards/reader.mjs +78 -2
- package/scripts/lib/process-group.mjs +899 -0
- package/scripts/lib/quality-gate.mjs +107 -28
- package/scripts/lib/reconcile/backlog.mjs +368 -0
- package/scripts/lib/reconcile/engine.mjs +55 -188
- package/scripts/lib/reconcile/rule-expiry-sweep.mjs +302 -60
- package/scripts/lib/reconcile/sanitize.mjs +69 -3
- package/scripts/lib/reconcile-nudge-banner.mjs +138 -45
- package/scripts/lib/resource-probe/parsers.mjs +31 -0
- package/scripts/lib/rule-loader.mjs +41 -12
- package/scripts/lib/scope-echo.mjs +39 -2
- package/scripts/lib/scope-gate.mjs +605 -1
- package/scripts/lib/session-close-backfill.mjs +33 -6
- package/scripts/lib/session-id.mjs +9 -20
- package/scripts/lib/session-invocation.mjs +20 -0
- package/scripts/lib/session-schema/constants.mjs +30 -2
- package/scripts/lib/session-schema/normalizer.mjs +56 -4
- package/scripts/lib/session-schema.mjs +8 -3
- package/scripts/lib/session-start-probes.mjs +95 -10
- package/scripts/lib/sessions-canonical.mjs +23 -0
- package/scripts/lib/sessions-integrity-banner.mjs +7 -1
- package/scripts/lib/sessions-staleness-banner.mjs +193 -51
- package/scripts/lib/skill-evidence-window.mjs +891 -0
- package/scripts/lib/skill-evolution/candidate-intake.mjs +133 -12
- package/scripts/lib/skill-evolution/engine.mjs +18 -9
- package/scripts/lib/skill-judge.mjs +45 -3
- package/scripts/lib/tail-window.mjs +56 -0
- package/scripts/lib/telemetry/schema.mjs +30 -0
- package/scripts/lib/telemetry/sync.mjs +61 -6
- package/scripts/lib/telemetry-flush-health-banner.mjs +4 -22
- package/scripts/lib/test-runner/issue-reconcile.mjs +48 -16
- package/scripts/lib/tmux-layout/telemetry-stats.mjs +72 -13
- package/scripts/lib/user-invocable-skills.mjs +23 -3
- package/scripts/lib/ux-grill/reconcile.mjs +48 -22
- package/scripts/lib/validate/check-agents-skills.mjs +26 -15
- package/scripts/lib/validate/check-cursor-adapter.mjs +1 -0
- package/scripts/lib/validate/check-entry-guard.mjs +13 -50
- package/scripts/lib/validate/check-hook-entry-guards.mjs +636 -0
- package/scripts/lib/validate/check-pi-prompts.mjs +1 -0
- package/scripts/lib/validate/check-rules.mjs +7 -5
- package/scripts/lib/validate/check-skill-links.mjs +9 -1
- package/scripts/lib/validate/check-skill-script-paths.mjs +239 -27
- package/scripts/lib/validate/check-test-git-config-target.mjs +24 -34
- package/scripts/lib/validate/check-untracked-test-deps.mjs +7 -102
- package/scripts/lib/validate/check-unwired-features.mjs +130 -27
- package/scripts/lib/validate/check-validator-registration.mjs +34 -10
- package/scripts/lib/validate/confidential-names.mjs +10 -0
- package/scripts/lib/validate-vendored-rules.mjs +4 -3
- package/scripts/lib/vault-mirror/namespace.mjs +46 -8
- package/scripts/lib/vault-mirror/process.mjs +10 -3
- package/scripts/lib/vault-mirror/render-sessions.mjs +12 -2
- package/scripts/lib/vault-status/narrative-mirror.mjs +31 -7
- package/scripts/lib/vault-yaml.mjs +118 -0
- package/scripts/lib/worktree/lifecycle.mjs +153 -1
- package/scripts/release-session-lock.mjs +305 -0
- package/scripts/release.mjs +30 -5
- package/scripts/resolve-session-invocation.mjs +59 -0
- package/scripts/run-quality-gate.mjs +156 -17
- package/scripts/sweep-expired-rules.mjs +14 -3
- package/scripts/validate-plugin.mjs +12 -0
- package/scripts/validate-wave-scope.mjs +32 -105
- package/scripts/vault-mirror.mjs +9 -1
- package/skills/_shared/platform-tools.md +23 -11
- package/skills/autopilot/SKILL.md +22 -7
- package/skills/claude-md-drift-check/SKILL.md +1 -1
- package/skills/convergence-monitoring/README.md +8 -1
- package/skills/convergence-monitoring/SIGNALS.md +50 -6
- package/skills/convergence-monitoring/SKILL.md +15 -6
- package/skills/eval/SKILL.md +39 -24
- package/skills/eval/rubric-v1.md +1 -0
- package/skills/eval/rubric-v2.md +457 -0
- package/skills/evolve/SKILL.md +1 -1
- package/skills/evolve/references/evolve-dialectic-mode.md +42 -25
- package/skills/gitlab-ops/SKILL.md +3 -2
- package/skills/npm-publish/SKILL.md +1 -1
- package/skills/reconcile/SKILL.md +11 -0
- package/skills/session-end/SKILL.md +13 -16
- package/skills/session-end/discovery-scan.md +1 -1
- package/skills/session-end/phase-3-6-tail.md +55 -9
- package/skills/session-end/references/phase-5-issue-cleanup.md +9 -14
- package/skills/session-end/session-metrics-write.md +10 -0
- package/skills/session-plan/SKILL.md +17 -5
- package/skills/session-plan/references/session-plan-task-classification.md +2 -2
- package/skills/session-start/references/phase-4-ssot-environment-check.md +2 -1
- package/skills/ux-grill/SKILL.md +1 -1
- package/skills/wave-executor/SKILL.md +8 -4
- package/skills/wave-executor/circuit-breaker.md +2 -0
- package/skills/wave-executor/references/wave-executor-state-init.md +5 -3
- package/skills/wave-executor/references/wave-loop-dispatch.md +2 -1
- package/.codex-plugin/skills/convergence-monitoring/agents/openai.yaml +0 -5
- package/.codex-plugin/skills/npm-publish/agents/openai.yaml +0 -5
- package/.cursor/commands/convergence-monitoring.md +0 -13
- package/.cursor/commands/npm-publish.md +0 -13
- package/pi/prompts/convergence-monitoring.md +0 -11
- package/pi/prompts/npm-publish.md +0 -11
|
@@ -233,50 +233,16 @@ export const CRITERION_IDS = Object.freeze(Object.keys(CRITERIA));
|
|
|
233
233
|
*/
|
|
234
234
|
export const TOTAL_WEIGHT = CRITERION_IDS.reduce((sum, id) => sum + CRITERIA[id].weight, 0);
|
|
235
235
|
|
|
236
|
-
// ---------------------------------------------------------------------------
|
|
237
|
-
// Die zwei harten Hürden
|
|
238
|
-
// ---------------------------------------------------------------------------
|
|
239
|
-
|
|
240
|
-
/**
|
|
241
|
-
* Hürden sind KEINE gewichteten Kriterien. Eine gerissene Hürde ergibt Note F,
|
|
242
|
-
* unabhängig von der Punktzahl — deshalb stehen sie getrennt und werden in
|
|
243
|
-
* `AuqScore.hurdlesBroken` geführt, nicht in `points` verrechnet.
|
|
244
|
-
*
|
|
245
|
-
* @type {Readonly<Record<'H1'|'H2', Readonly<{id:string,title:string,rule:string,criterion:string,evidence:string}>>>}
|
|
246
|
-
*/
|
|
247
|
-
export const HURDLES = Object.freeze({
|
|
248
|
-
H1: Object.freeze({
|
|
249
|
-
id: 'H1',
|
|
250
|
-
title: 'Kopfzeile höchstens 12 Zeichen',
|
|
251
|
-
rule: 'codepointLength(header) <= 12',
|
|
252
|
-
criterion: 'K5',
|
|
253
|
-
evidence:
|
|
254
|
-
'Gemessen 2026-08-22: 26 von 42 Kopfzeilen-Literalen reißen diese Grenze (62 %), ' +
|
|
255
|
-
'Spitzenwert 54 Zeichen. Die 12 ist die Stilangabe der Tool-Beschreibung ' +
|
|
256
|
-
'(`max 12 chars`), KEINE erzwungene Grenze: im Bundle 2.1.268 steht die Zahl nur ' +
|
|
257
|
-
'in ebendieser Beschreibung, es gibt kein `.max(12)` im Zod-Schema und keinen ' +
|
|
258
|
-
'Render-Pfad, der sie liest — 125 längere Kopfzeilen wurden vom Tool angenommen ' +
|
|
259
|
-
'und beantwortet (gemessen 2026-09-11). Deshalb gilt H1 nur für VORLAGEN, wo ein ' +
|
|
260
|
-
'Autor kostenlos kürzen kann; zur Laufzeit meldet der Hook sie und blockt nicht ' +
|
|
261
|
-
'(siehe BLOCKING_HURDLES in hooks/pre-auq-clarity.mjs).',
|
|
262
|
-
}),
|
|
263
|
-
H2: Object.freeze({
|
|
264
|
-
id: 'H2',
|
|
265
|
-
title: '2–4 Optionen je Frage, genau eine Empfehlung auf Platz 1',
|
|
266
|
-
rule: 'options.length zwischen 2 und 4 JE FRAGE, und isRecommended nur bei index 0',
|
|
267
|
-
criterion: 'K6',
|
|
268
|
-
evidence:
|
|
269
|
-
'Pro Block gezählt meldet skills/plan/SKILL.md:137 fälschlich 14 Optionen — ' +
|
|
270
|
-
'es ist ein legaler Vierfragen-Block. Der Zähler zählt je Frage.',
|
|
271
|
-
}),
|
|
272
|
-
});
|
|
273
|
-
|
|
274
|
-
/** Stabile Reihenfolge. */
|
|
275
|
-
export const HURDLE_IDS = Object.freeze(Object.keys(HURDLES));
|
|
276
|
-
|
|
277
236
|
// ---------------------------------------------------------------------------
|
|
278
237
|
// Schwellen — die einzige Stelle, an der eine Zahl steht
|
|
279
238
|
// ---------------------------------------------------------------------------
|
|
239
|
+
//
|
|
240
|
+
// DEFINITIONSREIHENFOLGE IST TRAGEND: `HURDLES` weiter unten baut seine
|
|
241
|
+
// operator-sichtbaren Sätze per Template-Literal aus `THRESHOLDS`, und ein
|
|
242
|
+
// Template-Literal wird beim Laden des Moduls ausgewertet. Steht `THRESHOLDS`
|
|
243
|
+
// wieder unter `HURDLES`, wirft der Import mit `ReferenceError` — auf einem
|
|
244
|
+
// Modul, das `hooks/pre-auq-clarity.mjs` in JEDER Session lädt, ist das eine
|
|
245
|
+
// host-weite Sperre, kein Testfehler.
|
|
280
246
|
|
|
281
247
|
/**
|
|
282
248
|
* Alle Schwellen, nach Kriterium gruppiert. clarity.mjs liest ausschließlich
|
|
@@ -356,6 +322,55 @@ export const THRESHOLDS = Object.freeze({
|
|
|
356
322
|
}),
|
|
357
323
|
});
|
|
358
324
|
|
|
325
|
+
// ---------------------------------------------------------------------------
|
|
326
|
+
// Die zwei harten Hürden
|
|
327
|
+
// ---------------------------------------------------------------------------
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* Hürden sind KEINE gewichteten Kriterien. Eine gerissene Hürde ergibt Note F,
|
|
331
|
+
* unabhängig von der Punktzahl — deshalb stehen sie getrennt und werden in
|
|
332
|
+
* `AuqScore.hurdlesBroken` geführt, nicht in `points` verrechnet.
|
|
333
|
+
*
|
|
334
|
+
* `title` und `rule` werden dem Operator GEDRUCKT (check-auq-clarity.mjs,
|
|
335
|
+
* auq-audit.mjs, der Deny-Text von hooks/pre-auq-clarity.mjs). Deshalb steht
|
|
336
|
+
* keine Zahl darin, sondern die Schwelle selbst: eine verschobene Schwelle mit
|
|
337
|
+
* einem stehengebliebenen Satz daneben ist eine Zeile, die den Operator belügt.
|
|
338
|
+
*
|
|
339
|
+
* @type {Readonly<Record<'H1'|'H2', Readonly<{id:string,title:string,rule:string,criterion:string,evidence:string}>>>}
|
|
340
|
+
*/
|
|
341
|
+
export const HURDLES = Object.freeze({
|
|
342
|
+
H1: Object.freeze({
|
|
343
|
+
id: 'H1',
|
|
344
|
+
title: `Kopfzeile höchstens ${THRESHOLDS.K5.headerCharsFail} Zeichen`,
|
|
345
|
+
rule: `codepointLength(header) <= ${THRESHOLDS.K5.headerCharsFail}`,
|
|
346
|
+
criterion: 'K5',
|
|
347
|
+
// `evidence` zitiert die Tool-Beschreibung des Bundles wörtlich
|
|
348
|
+
// (`max 12 chars`, `.max(12)`). Diese 12 ist die Zahl DES BUNDLES, nicht
|
|
349
|
+
// unsere Schwelle — sie bleibt ein Literal, auch wenn K5 sich bewegt.
|
|
350
|
+
evidence:
|
|
351
|
+
'Gemessen 2026-08-22: 26 von 42 Kopfzeilen-Literalen reißen diese Grenze (62 %), ' +
|
|
352
|
+
'Spitzenwert 54 Zeichen. Die 12 ist die Stilangabe der Tool-Beschreibung ' +
|
|
353
|
+
'(`max 12 chars`), KEINE erzwungene Grenze: im Bundle 2.1.268 steht die Zahl nur ' +
|
|
354
|
+
'in ebendieser Beschreibung, es gibt kein `.max(12)` im Zod-Schema und keinen ' +
|
|
355
|
+
'Render-Pfad, der sie liest — 125 längere Kopfzeilen wurden vom Tool angenommen ' +
|
|
356
|
+
'und beantwortet (gemessen 2026-09-11). Deshalb gilt H1 nur für VORLAGEN, wo ein ' +
|
|
357
|
+
'Autor kostenlos kürzen kann; zur Laufzeit meldet der Hook sie und blockt nicht ' +
|
|
358
|
+
'(siehe BLOCKING_HURDLES in hooks/pre-auq-clarity.mjs).',
|
|
359
|
+
}),
|
|
360
|
+
H2: Object.freeze({
|
|
361
|
+
id: 'H2',
|
|
362
|
+
title: `${THRESHOLDS.K6.optionsMin}–${THRESHOLDS.K6.optionsMax} Optionen je Frage, genau eine Empfehlung auf Platz 1`,
|
|
363
|
+
rule: `options.length zwischen ${THRESHOLDS.K6.optionsMin} und ${THRESHOLDS.K6.optionsMax} JE FRAGE, und isRecommended nur bei index 0`,
|
|
364
|
+
criterion: 'K6',
|
|
365
|
+
evidence:
|
|
366
|
+
'Pro Block gezählt meldet skills/plan/SKILL.md:137 fälschlich 14 Optionen — ' +
|
|
367
|
+
'es ist ein legaler Vierfragen-Block. Der Zähler zählt je Frage.',
|
|
368
|
+
}),
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
/** Stabile Reihenfolge. */
|
|
372
|
+
export const HURDLE_IDS = Object.freeze(Object.keys(HURDLES));
|
|
373
|
+
|
|
359
374
|
/** Höchstlänge eines Zitats in einem Befund (Codepoints). */
|
|
360
375
|
export const EXCERPT_MAX_CHARS = 80;
|
|
361
376
|
|
|
@@ -1,11 +1,24 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* auto-dialectic.mjs — Cadence helper for
|
|
2
|
+
* auto-dialectic.mjs — Cadence helper for the dialectic derivation (#506, F2.5).
|
|
3
3
|
*
|
|
4
|
-
* Mirrors the auto-dream.mjs API shape exactly. Decides whether
|
|
5
|
-
*
|
|
4
|
+
* Mirrors the auto-dream.mjs API shape exactly. Decides whether a dialectic
|
|
5
|
+
* derivation is DUE, writes the proposed peer-card diff to
|
|
6
6
|
* `.orchestrator/dialectic-pending.md` atomically, and tracks the last-run
|
|
7
|
-
* timestamp at `.orchestrator/dialectic-last-run
|
|
8
|
-
*
|
|
7
|
+
* timestamp at `.orchestrator/dialectic-last-run`.
|
|
8
|
+
*
|
|
9
|
+
* Who calls what (the session-end Phase 3.6.7 auto-trigger is GONE — #1288):
|
|
10
|
+
* - `shouldDispatchAutoDialectic` — read by the session-start `maintenance-due`
|
|
11
|
+
* probe (`scripts/lib/maintenance-due-banner.mjs`) to report the `dialectic`
|
|
12
|
+
* signal. Side-effect-free by contract: a variant advancing the last-run stamp
|
|
13
|
+
* would consume the signal it reports.
|
|
14
|
+
* - `writeDialecticPending` / `consumeDialecticPending` / `writeDialecticLastRun` —
|
|
15
|
+
* called by `/evolve dialectic` (Step 6.4 in
|
|
16
|
+
* `skills/evolve/references/evolve-dialectic-mode.md`), the only trigger today.
|
|
17
|
+
* - `renderPendingBody` / `comparePendingBody` (#1386) — the same Step 6.4 builds
|
|
18
|
+
* the sidecar body with the former on the dry-run path and, on `--apply`,
|
|
19
|
+
* compares the freshly derived body against the reviewed sidecar with the
|
|
20
|
+
* latter before writing any peer card.
|
|
21
|
+
* - Both sidecars are READ by the same `maintenance-due` probe.
|
|
9
22
|
*
|
|
10
23
|
* Decision inputs (PRD F2.5 acceptance criteria):
|
|
11
24
|
* - dialectic.cadence (default 5) — sessions since last dialectic run
|
|
@@ -17,7 +30,7 @@
|
|
|
17
30
|
* separate helpers. No external deps — Node 20+ stdlib only.
|
|
18
31
|
*/
|
|
19
32
|
|
|
20
|
-
import { readFile, writeFile, rename, mkdir } from 'node:fs/promises';
|
|
33
|
+
import { readFile, writeFile, rename, unlink, mkdir, readdir, stat, lstat } from 'node:fs/promises';
|
|
21
34
|
import { existsSync } from 'node:fs';
|
|
22
35
|
import { randomUUID } from 'node:crypto';
|
|
23
36
|
import path from 'node:path';
|
|
@@ -37,6 +50,20 @@ export const DIALECTIC_LAST_RUN_PATH = '.orchestrator/dialectic-last-run';
|
|
|
37
50
|
/** Repo-relative path to the pending dialectic proposal sidecar. */
|
|
38
51
|
export const DIALECTIC_PENDING_PATH = '.orchestrator/dialectic-pending.md';
|
|
39
52
|
|
|
53
|
+
/** Repo-relative directory the consumed sidecars are archived into (#1388 P9). */
|
|
54
|
+
export const DIALECTIC_CONSUMED_DIR = '.orchestrator/consumed';
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Retention ceiling for `.orchestrator/consumed/` — newest N archived sidecars
|
|
58
|
+
* survive a consume, older ones are pruned.
|
|
59
|
+
*
|
|
60
|
+
* Ceiling + revisit trigger (BV-004): newest 10; revisit if `checkStaleArtifacts`
|
|
61
|
+
* (`scripts/lib/project-hygiene.mjs`) ever reports a `consumed/` file. That probe
|
|
62
|
+
* counts every untracked `.orchestrator/**` file older than 30 days, so an
|
|
63
|
+
* unbounded archive would become a standing hygiene finding.
|
|
64
|
+
*/
|
|
65
|
+
export const DIALECTIC_CONSUMED_RETENTION = 10;
|
|
66
|
+
|
|
40
67
|
// ---------------------------------------------------------------------------
|
|
41
68
|
// Path helpers
|
|
42
69
|
// ---------------------------------------------------------------------------
|
|
@@ -49,6 +76,10 @@ function pendingPath(repoRoot) {
|
|
|
49
76
|
return path.join(repoRoot, DIALECTIC_PENDING_PATH);
|
|
50
77
|
}
|
|
51
78
|
|
|
79
|
+
function consumedDirPath(repoRoot) {
|
|
80
|
+
return path.join(repoRoot, DIALECTIC_CONSUMED_DIR);
|
|
81
|
+
}
|
|
82
|
+
|
|
52
83
|
function sessionsJsonlPath(repoRoot) {
|
|
53
84
|
return path.join(repoRoot, '.orchestrator', 'metrics', 'sessions.jsonl');
|
|
54
85
|
}
|
|
@@ -180,15 +211,16 @@ export async function readDialecticSignals({ repoRoot } = {}) {
|
|
|
180
211
|
// ---------------------------------------------------------------------------
|
|
181
212
|
|
|
182
213
|
/**
|
|
183
|
-
* Decide whether
|
|
184
|
-
* `/evolve
|
|
214
|
+
* Decide whether a dialectic derivation is DUE — i.e. whether the operator should
|
|
215
|
+
* run `/evolve dialectic` (dry-run first). Read by the session-start
|
|
216
|
+
* `maintenance-due` probe; no auto-trigger consumes this any more (#1288).
|
|
185
217
|
*
|
|
186
218
|
* Rules (PRD F2.5):
|
|
187
219
|
* - cadence === 0 → never trigger (kill-switch).
|
|
188
220
|
* - AC4 precondition: sessionsSinceLast === 0 AND learningsSinceLast === 0 →
|
|
189
221
|
* no new input since last run → skip with reason
|
|
190
|
-
* `no-new-input-since-last-run` (
|
|
191
|
-
*
|
|
222
|
+
* `no-new-input-since-last-run` (the reason string is part of the contract —
|
|
223
|
+
* the `maintenance-due` probe reports it verbatim).
|
|
192
224
|
* - sessionsSinceLast >= cadence → trigger (cadence threshold met).
|
|
193
225
|
* - Otherwise → skip with reason `under-threshold (sessions=N/M)`.
|
|
194
226
|
*
|
|
@@ -257,8 +289,12 @@ export async function shouldDispatchAutoDialectic({
|
|
|
257
289
|
* never a half-written intermediate (mirrors auto-dream.mjs:248-251).
|
|
258
290
|
*
|
|
259
291
|
* Defensive: returns `{ok: false, error}` on filesystem failure rather than
|
|
260
|
-
* throwing —
|
|
261
|
-
*
|
|
292
|
+
* throwing — the caller logs the error and continues.
|
|
293
|
+
*
|
|
294
|
+
* Caller: `/evolve dialectic` Step 6.4 (`skills/evolve/references/evolve-dialectic-mode.md`),
|
|
295
|
+
* after a successful `--apply` AND after the operator explicitly discards a
|
|
296
|
+
* proposal. Without this write the maintenance-due `dialectic` signal never
|
|
297
|
+
* resets (#1380). The session-end auto-trigger that used to call it is gone.
|
|
262
298
|
*
|
|
263
299
|
* @param {object} args
|
|
264
300
|
* @param {string} args.repoRoot
|
|
@@ -293,11 +329,17 @@ export async function writeDialecticLastRun({ repoRoot, isoTimestamp } = {}) {
|
|
|
293
329
|
* Write the proposed dialectic diff to `.orchestrator/dialectic-pending.md`
|
|
294
330
|
* atomically.
|
|
295
331
|
*
|
|
332
|
+
* Caller: `/evolve dialectic` Step 6.4's dry-run branch
|
|
333
|
+
* (`skills/evolve/references/evolve-dialectic-mode.md`) — `runDialecticDeriver()`
|
|
334
|
+
* returns the diff and the caller persists it here. There is no session-end
|
|
335
|
+
* auto-trigger any more (#1288).
|
|
336
|
+
*
|
|
296
337
|
* Caller supplies the body (a Markdown document containing the peer-card
|
|
297
338
|
* diff and any narrative). This helper prepends a minimal hand-rolled YAML
|
|
298
|
-
* frontmatter block carrying the metadata
|
|
299
|
-
*
|
|
300
|
-
*
|
|
339
|
+
* frontmatter block carrying the metadata the operator's review and the later
|
|
340
|
+
* `--apply` step rely on; the session-start `maintenance-due` probe reads the
|
|
341
|
+
* resulting file's presence + age for its `pending-sidecar` signal. The
|
|
342
|
+
* frontmatter is hand rolled (no js-yaml dep) — auto-dream pattern.
|
|
301
343
|
*
|
|
302
344
|
* Atomicity: tmp+rename (mirrors auto-dream.mjs:248-251).
|
|
303
345
|
*
|
|
@@ -366,6 +408,146 @@ export async function writeDialecticPending({
|
|
|
366
408
|
return { path: target, bytes: Buffer.byteLength(content, 'utf8') };
|
|
367
409
|
}
|
|
368
410
|
|
|
411
|
+
/**
|
|
412
|
+
* Archive file name for a consumed sidecar: `<ISO-stamp>-<8 hex>-dialectic-pending.md`.
|
|
413
|
+
* The stamp is `toISOString()` with `:` and `.` replaced by `-` — both are
|
|
414
|
+
* illegal or awkward on several filesystems, and the form still sorts
|
|
415
|
+
* lexicographically. The random suffix separates two consumes landing in the
|
|
416
|
+
* SAME millisecond — without it the second rename would overwrite the first
|
|
417
|
+
* archive silently.
|
|
418
|
+
*
|
|
419
|
+
* @returns {string}
|
|
420
|
+
*/
|
|
421
|
+
function archiveFileName() {
|
|
422
|
+
const stamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
423
|
+
return `${stamp}-${randomUUID().slice(0, 8)}-dialectic-pending.md`;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
/**
|
|
427
|
+
* Exactly the names {@link archiveFileName} produces. The prune touches nothing
|
|
428
|
+
* else in the directory (#1390 P6): a loose suffix match would still delete an
|
|
429
|
+
* unrelated `operator-dialectic-pending.md`.
|
|
430
|
+
*/
|
|
431
|
+
const ARCHIVE_NAME_RE =
|
|
432
|
+
/^\d{4}-\d{2}-\d{2}T\d{2}-\d{2}-\d{2}-\d{3}Z-[0-9a-f]{8}-dialectic-pending\.md$/;
|
|
433
|
+
|
|
434
|
+
/**
|
|
435
|
+
* Prune `.orchestrator/consumed/` to the newest `DIALECTIC_CONSUMED_RETENTION`
|
|
436
|
+
* archives. Only regular files whose name matches {@link ARCHIVE_NAME_RE} are
|
|
437
|
+
* counted or removed — anything else in the directory is not this module's to
|
|
438
|
+
* delete. Best-effort: every failure degrades to "pruned nothing" — an archive
|
|
439
|
+
* that grew one file too long is never worth failing a consume over.
|
|
440
|
+
*
|
|
441
|
+
* "Newest" is mtime DESC with the filename as tiebreaker: the archive names
|
|
442
|
+
* carry an ISO timestamp, which sorts lexicographically, so the two orderings
|
|
443
|
+
* agree except inside one millisecond.
|
|
444
|
+
*
|
|
445
|
+
* @param {string} dir Absolute path to the consumed archive directory.
|
|
446
|
+
* @returns {Promise<number>} Number of files removed.
|
|
447
|
+
*/
|
|
448
|
+
async function pruneConsumedArchive(dir) {
|
|
449
|
+
let removed = 0;
|
|
450
|
+
try {
|
|
451
|
+
const entries = await readdir(dir, { withFileTypes: true });
|
|
452
|
+
const files = entries
|
|
453
|
+
.filter((e) => e.isFile() && ARCHIVE_NAME_RE.test(e.name))
|
|
454
|
+
.map((e) => e.name);
|
|
455
|
+
if (files.length <= DIALECTIC_CONSUMED_RETENTION) return 0;
|
|
456
|
+
|
|
457
|
+
const dated = [];
|
|
458
|
+
for (const name of files) {
|
|
459
|
+
let mtimeMs = 0;
|
|
460
|
+
try {
|
|
461
|
+
mtimeMs = (await stat(path.join(dir, name))).mtimeMs;
|
|
462
|
+
} catch {
|
|
463
|
+
mtimeMs = 0; // unreadable → oldest, pruned first
|
|
464
|
+
}
|
|
465
|
+
dated.push({ name, mtimeMs });
|
|
466
|
+
}
|
|
467
|
+
dated.sort((a, b) => b.mtimeMs - a.mtimeMs || (a.name < b.name ? 1 : -1));
|
|
468
|
+
|
|
469
|
+
for (const { name } of dated.slice(DIALECTIC_CONSUMED_RETENTION)) {
|
|
470
|
+
try {
|
|
471
|
+
await unlink(path.join(dir, name));
|
|
472
|
+
removed += 1;
|
|
473
|
+
} catch {
|
|
474
|
+
/* best-effort */
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
} catch {
|
|
478
|
+
/* best-effort — a prune failure never fails the consume */
|
|
479
|
+
}
|
|
480
|
+
return removed;
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
/**
|
|
484
|
+
* Consume `.orchestrator/dialectic-pending.md` once its proposal has been
|
|
485
|
+
* applied or explicitly discarded: the file is MOVED into
|
|
486
|
+
* `.orchestrator/consumed/<ISO-timestamp>-dialectic-pending.md` rather than
|
|
487
|
+
* unlinked (#1388 P9 — the sidecar was the only copy of what the operator
|
|
488
|
+
* reviewed, and an unlink destroyed it). The maintenance-due probe reads an
|
|
489
|
+
* explicit two-path list (`PENDING_SIDECARS`,
|
|
490
|
+
* `scripts/lib/maintenance-due-banner.mjs`), so an archived file does not
|
|
491
|
+
* re-raise `pending-sidecar` (#1380 stays fixed).
|
|
492
|
+
*
|
|
493
|
+
* The archive is pruned to `DIALECTIC_CONSUMED_RETENTION` entries on every
|
|
494
|
+
* consume — see that constant for the ceiling and its revisit trigger.
|
|
495
|
+
*
|
|
496
|
+
* ENOENT is tolerated (`consumed: false`) — "already gone" is the desired end
|
|
497
|
+
* state. Any other filesystem error returns `{ok: false, error}` rather than
|
|
498
|
+
* throwing, matching `writeDialecticLastRun`.
|
|
499
|
+
*
|
|
500
|
+
* Symlink (#1390 P6, measured 2026-09-19): a pre-existing `.orchestrator/consumed`
|
|
501
|
+
* symlink used to be followed — `mkdir` was a no-op on it, `rename` moved the
|
|
502
|
+
* sidecar into its target, and the retention prune unlinked that target's
|
|
503
|
+
* regular files beyond the newest 10 (a tmp repro deleted 3 of 12). The consume
|
|
504
|
+
* now `lstat`s the directory first and REFUSES a symlink: it returns
|
|
505
|
+
* `{ok: false, error}`, moves nothing and prunes nothing, so the pending
|
|
506
|
+
* sidecar stays where it is for the caller to report. The prune additionally
|
|
507
|
+
* only ever removes names this module writes (see `ARCHIVE_NAME_RE`). Ceiling:
|
|
508
|
+
* the `lstat` → `rename` window is not atomic — a process swapping in a symlink
|
|
509
|
+
* inside it wins; revisit if the consumed directory ever becomes writable by a
|
|
510
|
+
* less-trusted uid than the operator's.
|
|
511
|
+
*
|
|
512
|
+
* @param {object} args
|
|
513
|
+
* @param {string} args.repoRoot
|
|
514
|
+
* @returns {Promise<{ok:boolean, consumed?:boolean, error?:string, path?:string, archivedTo?:string, pruned?:number}>}
|
|
515
|
+
*/
|
|
516
|
+
export async function consumeDialecticPending({ repoRoot } = {}) {
|
|
517
|
+
if (!repoRoot) {
|
|
518
|
+
return { ok: false, error: 'consumeDialecticPending: repoRoot is required' };
|
|
519
|
+
}
|
|
520
|
+
const target = pendingPath(repoRoot);
|
|
521
|
+
if (!existsSync(target)) return { ok: true, consumed: false, path: target };
|
|
522
|
+
|
|
523
|
+
const dir = consumedDirPath(repoRoot);
|
|
524
|
+
const archivedTo = path.join(dir, archiveFileName());
|
|
525
|
+
|
|
526
|
+
try {
|
|
527
|
+
let dirStat = null;
|
|
528
|
+
try {
|
|
529
|
+
dirStat = await lstat(dir);
|
|
530
|
+
} catch (err) {
|
|
531
|
+
if (err?.code !== 'ENOENT') throw err;
|
|
532
|
+
}
|
|
533
|
+
if (dirStat?.isSymbolicLink()) {
|
|
534
|
+
return {
|
|
535
|
+
ok: false,
|
|
536
|
+
path: target,
|
|
537
|
+
error: `consumeDialecticPending: ${DIALECTIC_CONSUMED_DIR} is a symlink — refusing to archive or prune through it`,
|
|
538
|
+
};
|
|
539
|
+
}
|
|
540
|
+
await mkdir(dir, { recursive: true });
|
|
541
|
+
await rename(target, archivedTo);
|
|
542
|
+
} catch (err) {
|
|
543
|
+
if (err?.code === 'ENOENT') return { ok: true, consumed: false, path: target };
|
|
544
|
+
return { ok: false, error: err.message };
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
const pruned = await pruneConsumedArchive(dir);
|
|
548
|
+
return { ok: true, consumed: true, path: target, archivedTo, pruned };
|
|
549
|
+
}
|
|
550
|
+
|
|
369
551
|
/**
|
|
370
552
|
* Read `.orchestrator/dialectic-pending.md` if present. Returns the raw file
|
|
371
553
|
* body (including frontmatter) so callers can decide how to parse it.
|
|
@@ -389,3 +571,110 @@ export async function readDialecticPending({ repoRoot } = {}) {
|
|
|
389
571
|
return null;
|
|
390
572
|
}
|
|
391
573
|
}
|
|
574
|
+
|
|
575
|
+
// ---------------------------------------------------------------------------
|
|
576
|
+
// pending body — serializer + drift comparison (#1386, variant b)
|
|
577
|
+
// ---------------------------------------------------------------------------
|
|
578
|
+
|
|
579
|
+
/**
|
|
580
|
+
* Serialize a deriver diff OBJECT (`result.diff = {user?, agent?}`) into the
|
|
581
|
+
* Markdown body `writeDialecticPending` persists.
|
|
582
|
+
*
|
|
583
|
+
* This lived as a JS snippet in `skills/evolve/references/evolve-dialectic-mode.md`
|
|
584
|
+
* Step 6.4 and is moved here VERBATIM so the dry-run write and the apply-time
|
|
585
|
+
* comparison build the body from the same code — two hand-copied serializers
|
|
586
|
+
* would report drift that is only formatting (#1386).
|
|
587
|
+
*
|
|
588
|
+
* Pure: no I/O, no throw. A non-object argument, or one with neither target as
|
|
589
|
+
* a string, yields `''` — the caller's "nothing to review, write no sidecar"
|
|
590
|
+
* branch.
|
|
591
|
+
*
|
|
592
|
+
* @param {{user?: string, agent?: string}} diff
|
|
593
|
+
* @returns {string} Fenced Markdown body, or `''` when nothing was proposed.
|
|
594
|
+
*/
|
|
595
|
+
export function renderPendingBody(diff) {
|
|
596
|
+
if (!diff || typeof diff !== 'object') return '';
|
|
597
|
+
const FENCE = '`'.repeat(3);
|
|
598
|
+
return ['user', 'agent']
|
|
599
|
+
.filter((t) => typeof diff[t] === 'string')
|
|
600
|
+
.map((t) => [`${FENCE}diff`, `# target: ${t}`, diff[t].trimEnd(), FENCE].join('\n'))
|
|
601
|
+
.join('\n\n');
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
/**
|
|
605
|
+
* Strip EXACTLY the leading frontmatter block `writeDialecticPending` wrote.
|
|
606
|
+
*
|
|
607
|
+
* The delimiters sit at fixed positions — line 0 and the next `---` LINE — so
|
|
608
|
+
* this splits on the first two delimiter LINES only. A naive `split('---')`
|
|
609
|
+
* breaks on a body containing a `---` line, and diff bodies do.
|
|
610
|
+
*
|
|
611
|
+
* @param {string} content
|
|
612
|
+
* @returns {string} The body, or the whole input when no frontmatter is present.
|
|
613
|
+
*/
|
|
614
|
+
function stripPendingFrontmatter(content) {
|
|
615
|
+
const lines = content.split('\n');
|
|
616
|
+
if (lines[0] !== '---') return content;
|
|
617
|
+
const end = lines.indexOf('---', 1);
|
|
618
|
+
if (end === -1) return content; // unterminated → nothing to strip
|
|
619
|
+
return lines.slice(end + 1).join('\n');
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
/**
|
|
623
|
+
* Whitespace normalisation for the drift comparison: exactly ONE trailing
|
|
624
|
+
* newline is removed from each side.
|
|
625
|
+
*
|
|
626
|
+
* `writeDialecticPending` appends a newline when the body lacks one, so the
|
|
627
|
+
* persisted body is the rendered body plus `\n` — without this normalisation a
|
|
628
|
+
* clean round-trip would always report drift. Nothing else is normalised (no
|
|
629
|
+
* trim, no line-ending or indentation folding): anything more would hide real
|
|
630
|
+
* drift in a diff body, where trailing whitespace is content.
|
|
631
|
+
*/
|
|
632
|
+
function normalizeTrailingNewline(text) {
|
|
633
|
+
return text.endsWith('\n') ? text.slice(0, -1) : text;
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
/**
|
|
637
|
+
* Compare a freshly derived pending body against the sidecar the operator
|
|
638
|
+
* reviewed (#1386, variant b).
|
|
639
|
+
*
|
|
640
|
+
* `/evolve dialectic --apply` re-derives from the model, so what gets applied
|
|
641
|
+
* is not necessarily what was approved in `.orchestrator/dialectic-pending.md`.
|
|
642
|
+
* This makes that divergence VISIBLE at apply time; it does not make apply
|
|
643
|
+
* deterministic.
|
|
644
|
+
*
|
|
645
|
+
* Never throws — every failure degrades to a result object, matching this
|
|
646
|
+
* module's other readers.
|
|
647
|
+
*
|
|
648
|
+
* @param {object} args
|
|
649
|
+
* @param {string} args.repoRoot
|
|
650
|
+
* @param {string} args.body Fresh body, typically from `renderPendingBody()`.
|
|
651
|
+
* @returns {Promise<{ok:boolean, drifted:boolean, sidecarAbsent:boolean, sidecarBody?:string, freshBody?:string, error?:string}>}
|
|
652
|
+
*/
|
|
653
|
+
export async function comparePendingBody({ repoRoot, body } = {}) {
|
|
654
|
+
if (!repoRoot) {
|
|
655
|
+
return {
|
|
656
|
+
ok: false,
|
|
657
|
+
drifted: false,
|
|
658
|
+
sidecarAbsent: false,
|
|
659
|
+
error: 'comparePendingBody: repoRoot is required',
|
|
660
|
+
};
|
|
661
|
+
}
|
|
662
|
+
if (typeof body !== 'string') {
|
|
663
|
+
return {
|
|
664
|
+
ok: false,
|
|
665
|
+
drifted: false,
|
|
666
|
+
sidecarAbsent: false,
|
|
667
|
+
error: 'comparePendingBody: body must be a string',
|
|
668
|
+
};
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
const raw = await readDialecticPending({ repoRoot });
|
|
672
|
+
// Applying without a sidecar is legitimate (the operator may never have run
|
|
673
|
+
// the dry-run) — that is NOT drift.
|
|
674
|
+
if (raw === null) return { ok: true, drifted: false, sidecarAbsent: true };
|
|
675
|
+
|
|
676
|
+
const sidecarBody = stripPendingFrontmatter(raw);
|
|
677
|
+
const drifted = normalizeTrailingNewline(sidecarBody) !== normalizeTrailingNewline(body);
|
|
678
|
+
|
|
679
|
+
return { ok: true, drifted, sidecarAbsent: false, sidecarBody, freshBody: body };
|
|
680
|
+
}
|
|
@@ -58,7 +58,7 @@ function parseNumeric(raw) {
|
|
|
58
58
|
* silently to bounds. Unknown flags are ignored. `--dry-run` is a boolean flag.
|
|
59
59
|
*
|
|
60
60
|
* @param {string[]} argv — argument tokens (e.g. ['--max-sessions=3', '--dry-run'])
|
|
61
|
-
* @returns {{maxSessions: number, maxHours: number, confidenceThreshold: number, dryRun: boolean}}
|
|
61
|
+
* @returns {{maxSessions: number, maxHours: number, confidenceThreshold: number, maxTokens: number, dryRun: boolean}}
|
|
62
62
|
*/
|
|
63
63
|
export function parseFlags(argv) {
|
|
64
64
|
const tokens = Array.isArray(argv) ? argv : [];
|
|
@@ -66,6 +66,7 @@ export function parseFlags(argv) {
|
|
|
66
66
|
let rawSessions = null;
|
|
67
67
|
let rawHours = null;
|
|
68
68
|
let rawConfidence = null;
|
|
69
|
+
let rawTokens = null;
|
|
69
70
|
let dryRun = false;
|
|
70
71
|
|
|
71
72
|
for (const tok of tokens) {
|
|
@@ -81,6 +82,7 @@ export function parseFlags(argv) {
|
|
|
81
82
|
if (key === '--max-sessions') rawSessions = parseNumeric(val);
|
|
82
83
|
else if (key === '--max-hours') rawHours = parseNumeric(val);
|
|
83
84
|
else if (key === '--confidence-threshold') rawConfidence = parseNumeric(val);
|
|
85
|
+
else if (key === '--max-tokens') rawTokens = parseNumeric(val);
|
|
84
86
|
}
|
|
85
87
|
|
|
86
88
|
return {
|
|
@@ -99,6 +101,15 @@ export function parseFlags(argv) {
|
|
|
99
101
|
max: FLAG_BOUNDS.confidenceThreshold.max,
|
|
100
102
|
fallback: FLAG_BOUNDS.confidenceThreshold.default,
|
|
101
103
|
}),
|
|
104
|
+
// Integer token count — the TOKEN_BUDGET_EXCEEDED switch compares it against
|
|
105
|
+
// a summed `usage.output_tokens`, which is never fractional. `0` disables the
|
|
106
|
+
// switch (`kill-switches.mjs` guards on `maxTokens > 0`), so the lower bound
|
|
107
|
+
// stays 0 rather than 1.
|
|
108
|
+
maxTokens: Math.floor(clampNumber(rawTokens, {
|
|
109
|
+
min: FLAG_BOUNDS.maxTokens.min,
|
|
110
|
+
max: FLAG_BOUNDS.maxTokens.max,
|
|
111
|
+
fallback: FLAG_BOUNDS.maxTokens.default,
|
|
112
|
+
})),
|
|
102
113
|
dryRun,
|
|
103
114
|
};
|
|
104
115
|
}
|
|
@@ -110,7 +110,8 @@ export function preIterationKillSwitch(args) {
|
|
|
110
110
|
* @param {object | null | undefined} sessionResult
|
|
111
111
|
* @param {object} opts
|
|
112
112
|
* @param {number} opts.carryoverThreshold
|
|
113
|
-
* @param {string} [opts.autopilotJsonlPath] — STALL_TIMEOUT sampler
|
|
113
|
+
* @param {string} [opts.autopilotJsonlPath] — STALL_TIMEOUT sampler fallback marker
|
|
114
|
+
* @param {string} [opts.sessionLockPath] — STALL_TIMEOUT sampler primary marker
|
|
114
115
|
* @param {number} [opts.stallTimeoutSeconds] — STALL_TIMEOUT threshold (default 600)
|
|
115
116
|
* @param {() => number} [opts.nowMs] — wall-clock supplier (DI seam for tests)
|
|
116
117
|
* @returns {{kill: string, detail: string} | null}
|
|
@@ -120,12 +121,14 @@ export function postSessionKillSwitch(sessionResult, opts) {
|
|
|
120
121
|
const { carryoverThreshold } = opts;
|
|
121
122
|
|
|
122
123
|
// STALL_TIMEOUT (ADR-364 §3, issue #371) — one-strike v1.
|
|
123
|
-
// Sampler
|
|
124
|
-
//
|
|
124
|
+
// Sampler prefers the session.lock heartbeat and falls back to autopilot.jsonl
|
|
125
|
+
// mtime; missing marker → stallSeconds=0 → no kill (documented contract:
|
|
126
|
+
// a missing file is NOT a kill condition).
|
|
125
127
|
// Events route to autopilot.jsonl, NOT failures.jsonl (ADR-364 cross-connections rule 4).
|
|
126
128
|
const stallTimeoutSeconds = opts.stallTimeoutSeconds ?? 600;
|
|
127
129
|
const stallSample = sampleProgress({
|
|
128
130
|
autopilotJsonlPath: opts.autopilotJsonlPath,
|
|
131
|
+
sessionLockPath: opts.sessionLockPath,
|
|
129
132
|
stallTimeoutSeconds,
|
|
130
133
|
nowMs: opts.nowMs,
|
|
131
134
|
});
|
|
@@ -59,7 +59,11 @@ function clampNumber(value, { min, max, fallback }) {
|
|
|
59
59
|
* @param {string} [opts.jsonlPath] @param {string} [opts.runId] @param {string} [opts.branch]
|
|
60
60
|
* @param {string} [opts.hostJsonPath] @param {number} [opts.peerAbortThreshold]
|
|
61
61
|
* @param {number} [opts.carryoverThreshold] @param {number} [opts.maxTokens]
|
|
62
|
-
* @param {string} [opts.autopilotJsonlPath] — STALL_TIMEOUT sampler
|
|
62
|
+
* @param {string} [opts.autopilotJsonlPath] — STALL_TIMEOUT sampler fallback marker (defaults to jsonlPath)
|
|
63
|
+
* @param {string} [opts.sessionLockPath] — STALL_TIMEOUT sampler PRIMARY marker (`session.lock`
|
|
64
|
+
* `last_heartbeat`). Deliberately undefaulted: the default would be cwd-relative, which in a
|
|
65
|
+
* test run resolves to the LIVE repo lock and would silently decide the sampler's branch. The
|
|
66
|
+
* CLI driver (`scripts/autopilot.mjs`) supplies it; omitting it keeps the legacy mtime marker.
|
|
63
67
|
* @param {number} [opts.stallTimeoutSeconds] — STALL_TIMEOUT threshold seconds (default 600)
|
|
64
68
|
* @param {string} [opts.worktreePath] @param {string} [opts.parentRunId]
|
|
65
69
|
* @param {number} [opts.blockedByIssue] — Phase D (#341) forward-compat: issue number this loop is waiting on (OPEN-4 commit-deps); callers may omit, defaults to null
|
|
@@ -125,6 +129,14 @@ export async function runLoop(opts = {}) {
|
|
|
125
129
|
max_sessions: maxSessions,
|
|
126
130
|
max_hours: maxHours,
|
|
127
131
|
confidence_threshold: confidenceThreshold,
|
|
132
|
+
// Persisted for the same reason as the three above (HR-105): a switch whose
|
|
133
|
+
// input is never recorded cannot be falsified afterwards. `0` = disabled.
|
|
134
|
+
// CEILING: reaching a NON-zero value here does not make TOKEN_BUDGET_EXCEEDED
|
|
135
|
+
// live under the headless driver — `readTailSession()` in scripts/autopilot.mjs
|
|
136
|
+
// returns no `usage`, and sessions.jsonl carries no token field to build one
|
|
137
|
+
// from, so `total_tokens_used` stays 0. Revisit when a session record gains
|
|
138
|
+
// token counts.
|
|
139
|
+
max_tokens: opts.maxTokens ?? 0,
|
|
128
140
|
iterations_completed: 0,
|
|
129
141
|
kill_switch: null,
|
|
130
142
|
kill_switch_detail: null,
|
|
@@ -275,6 +287,7 @@ export async function runLoop(opts = {}) {
|
|
|
275
287
|
const postCheck = postSessionKillSwitch(sessionResult, {
|
|
276
288
|
carryoverThreshold,
|
|
277
289
|
autopilotJsonlPath: opts.autopilotJsonlPath ?? jsonlPath,
|
|
290
|
+
sessionLockPath: opts.sessionLockPath,
|
|
278
291
|
stallTimeoutSeconds: opts.stallTimeoutSeconds ?? 600,
|
|
279
292
|
nowMs: opts.nowMs,
|
|
280
293
|
});
|