mandrel 2.0.0 → 2.2.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/README.md +59 -28
- package/.agents/agents/acceptance-critic.md +20 -9
- package/.agents/agents/story-worker.md +45 -48
- package/.agents/audit-checklists/performance.md +1 -1
- package/.agents/docs/SDLC.md +60 -46
- package/.agents/docs/agentrc-reference.json +8 -13
- package/.agents/docs/configuration.md +33 -57
- package/.agents/docs/execution-reference.md +39 -10
- package/.agents/docs/quality-gates.md +17 -19
- package/.agents/docs/workflows.md +6 -6
- package/.agents/instructions.md +64 -79
- package/.agents/rules/ci-remediation.md +3 -3
- package/.agents/rules/gherkin-standards.md +10 -0
- package/.agents/rules/git-conventions-reference.md +42 -51
- package/.agents/schemas/acceptance-eval-verdict.schema.json +2 -2
- package/.agents/schemas/agentrc.schema.json +35 -46
- package/.agents/schemas/audit-rules.json +59 -1
- package/.agents/schemas/audit-rules.schema.json +33 -1
- package/.agents/schemas/lifecycle/README.md +1 -2
- package/.agents/schemas/lifecycle/ledger-record.schema.json +1 -1
- package/.agents/schemas/lifecycle/merge.flip-failed.schema.json +33 -0
- package/.agents/schemas/lifecycle/merge.unlanded.schema.json +1 -0
- package/.agents/schemas/lifecycle/story.merged.schema.json +1 -1
- package/.agents/schemas/signal-event.schema.json +3 -3
- package/.agents/schemas/story-deliver-terminal.schema.json +152 -0
- package/.agents/schemas/validation-evidence.schema.json +1 -1
- package/.agents/scripts/acceptance-eval.js +24 -68
- package/.agents/scripts/agents-bootstrap-github.js +1 -1
- package/.agents/scripts/bootstrap.js +3 -3
- package/.agents/scripts/check-dead-exports.js +43 -104
- package/.agents/scripts/check-doc-links.js +2 -2
- package/.agents/scripts/check-lifecycle-lint.js +1 -1
- package/.agents/scripts/check-workflow-cli-lint.js +91 -0
- package/.agents/scripts/deliver-recover.js +122 -0
- package/.agents/scripts/drain-pending-cleanup.js +1 -1
- package/.agents/scripts/evidence-gate.js +20 -50
- package/.agents/scripts/generate-skills-index.js +17 -1
- package/.agents/scripts/generate-workflows-doc.js +4 -4
- package/.agents/scripts/lib/ITicketingProvider.js +1 -19
- package/.agents/scripts/lib/audit-suite/selector.js +323 -23
- package/.agents/scripts/lib/baselines/kinds/maintainability.js +0 -11
- package/.agents/scripts/lib/bootstrap/ci-workflow-template.js +28 -33
- package/.agents/scripts/lib/bootstrap/manifest.js +8 -11
- package/.agents/scripts/lib/bootstrap/project-bootstrap.js +30 -53
- package/.agents/scripts/lib/bootstrap/quality-bootstrap.js +0 -2
- package/.agents/scripts/lib/checks/core-bare-clean.js +4 -1
- package/.agents/scripts/lib/checks/index.js +1 -1
- package/.agents/scripts/lib/checks/loop-health.js +12 -11
- package/.agents/scripts/lib/checks/state.js +17 -248
- package/.agents/scripts/lib/checks/story-init-not-backgrounded.js +3 -3
- package/.agents/scripts/lib/checks/subagent-agent-tool-required.js +3 -4
- package/.agents/scripts/lib/checks/worktree-bootstrap-env.js +2 -2
- package/.agents/scripts/lib/checks/worktree-residue-biome.js +3 -3
- package/.agents/scripts/lib/cli-args.js +23 -2
- package/.agents/scripts/lib/close-validation/gates.js +13 -13
- package/.agents/scripts/lib/close-validation/projections/inputs.js +7 -7
- package/.agents/scripts/lib/close-validation/projections/maintainability.js +12 -12
- package/.agents/scripts/lib/close-validation/runner.js +13 -21
- package/.agents/scripts/lib/close-validation/telemetry.js +17 -8
- package/.agents/scripts/lib/config/acceptance-eval.js +2 -2
- package/.agents/scripts/lib/config/delivery-routing.js +7 -6
- package/.agents/scripts/lib/config/explain.js +10 -16
- package/.agents/scripts/lib/config/github.js +7 -5
- package/.agents/scripts/lib/config/limits.js +15 -25
- package/.agents/scripts/lib/config/quality.js +11 -14
- package/.agents/scripts/lib/config/runners.js +8 -21
- package/.agents/scripts/lib/config/temp-paths.js +18 -56
- package/.agents/scripts/lib/config-settings-schema-delivery.js +34 -16
- package/.agents/scripts/lib/config-settings-schema-quality.js +9 -2
- package/.agents/scripts/lib/config-settings-schema.js +48 -22
- package/.agents/scripts/lib/dead-exports-knip.js +105 -0
- package/.agents/scripts/lib/dead-exports-mode.js +51 -0
- package/.agents/scripts/lib/duplicate-search.js +38 -7
- package/.agents/scripts/lib/findings/promote-finding.js +23 -14
- package/.agents/scripts/lib/format-generated-json.js +97 -0
- package/.agents/scripts/lib/framework-version.js +19 -189
- package/.agents/scripts/lib/gh-exec.js +8 -0
- package/.agents/scripts/lib/git-branch-lifecycle.js +0 -158
- package/.agents/scripts/lib/git-utils.js +0 -14
- package/.agents/scripts/lib/json-utils.js +1 -2
- package/.agents/scripts/lib/label-constants.js +0 -15
- package/.agents/scripts/lib/label-taxonomy.js +1 -12
- package/.agents/scripts/lib/observability/active-story-env.js +42 -163
- package/.agents/scripts/lib/observability/runtime-friction.js +243 -0
- package/.agents/scripts/lib/observability/signal-validator.js +4 -4
- package/.agents/scripts/lib/observability/signals-writer.js +6 -82
- package/.agents/scripts/lib/observability/source-classifier.js +2 -2
- package/.agents/scripts/lib/observability/tool-trace-hook.js +2 -12
- package/.agents/scripts/lib/orchestration/acceptance-clusters.js +1 -1
- package/.agents/scripts/lib/orchestration/acceptance-eval-decision.js +2 -2
- package/.agents/scripts/lib/orchestration/ceremony-routing.js +43 -45
- package/.agents/scripts/lib/orchestration/change-set.js +103 -0
- package/.agents/scripts/lib/orchestration/code-review.js +70 -191
- package/.agents/scripts/lib/orchestration/consolidation-precondition.js +3 -3
- package/.agents/scripts/lib/orchestration/deliver-recover.js +328 -0
- package/.agents/scripts/lib/orchestration/detectors-phase.js +12 -6
- package/.agents/scripts/lib/orchestration/git-cleanup/phases/fast-forward.js +34 -0
- package/.agents/scripts/lib/orchestration/lease-guard-shared.js +3 -2
- package/.agents/scripts/lib/orchestration/lifecycle/emit-ledger-event.js +142 -0
- package/.agents/scripts/lib/orchestration/lifecycle/emit-loop-tick.js +9 -11
- package/.agents/scripts/lib/orchestration/lifecycle/emit-merge-flip-failed.js +86 -0
- package/.agents/scripts/lib/orchestration/lifecycle/emit-merge-unlanded.js +37 -103
- package/.agents/scripts/lib/orchestration/lifecycle/listeners/README.md +7 -3
- package/.agents/scripts/lib/orchestration/lifecycle/listeners/watcher.js +50 -85
- package/.agents/scripts/lib/orchestration/lifecycle/trace-logger.js +3 -14
- package/.agents/scripts/lib/orchestration/merge-block-class.js +76 -20
- package/.agents/scripts/lib/orchestration/merge-poll.js +104 -0
- package/.agents/scripts/lib/orchestration/plan-context.js +116 -33
- package/.agents/scripts/lib/orchestration/plan-critic-conditions.js +26 -36
- package/.agents/scripts/lib/orchestration/plan-critics-evaluate.js +31 -22
- package/.agents/scripts/lib/orchestration/plan-metrics.js +38 -6
- package/.agents/scripts/lib/orchestration/plan-persist/fan-out-gate.js +16 -6
- package/.agents/scripts/lib/orchestration/plan-persist/persist-helpers.js +173 -25
- package/.agents/scripts/lib/orchestration/plan-persist/plan-context-source.js +116 -0
- package/.agents/scripts/lib/orchestration/plan-persist/run-plan-persist.js +280 -100
- package/.agents/scripts/lib/orchestration/plan-persist/story-ops.js +472 -55
- package/.agents/scripts/lib/orchestration/plan-persist/summary.js +21 -16
- package/.agents/scripts/lib/orchestration/plan-persist/supersede-ops.js +509 -0
- package/.agents/scripts/lib/orchestration/plan-text-hygiene.js +230 -0
- package/.agents/scripts/lib/orchestration/planning/authoring-context.js +41 -40
- package/.agents/scripts/lib/orchestration/planning/decomposer-context.js +1 -2
- package/.agents/scripts/lib/orchestration/planning/spec-authoring-grounding.js +1 -1
- package/.agents/scripts/lib/orchestration/resolve-stories.js +344 -0
- package/.agents/scripts/lib/orchestration/retro-proposals.js +7 -7
- package/.agents/scripts/lib/orchestration/review-depth.js +105 -40
- package/.agents/scripts/lib/orchestration/review-providers/findings-renderer.js +3 -13
- package/.agents/scripts/lib/orchestration/review-providers/native.js +1 -154
- package/.agents/scripts/lib/orchestration/review-providers/review-depth.js +3 -2
- package/.agents/scripts/lib/orchestration/review-providers/security-review.js +1 -1
- package/.agents/scripts/lib/orchestration/review-providers/types.js +5 -4
- package/.agents/scripts/lib/orchestration/review-providers/ultrareview.js +1 -1
- package/.agents/scripts/lib/orchestration/run-epilogue.js +374 -16
- package/.agents/scripts/lib/orchestration/single-story-close/phases/auto-merge.js +24 -0
- package/.agents/scripts/lib/orchestration/single-story-close/phases/base-sync.js +11 -9
- package/.agents/scripts/lib/orchestration/single-story-close/phases/close-validation.js +4 -4
- package/.agents/scripts/lib/orchestration/single-story-close/phases/code-review.js +4 -13
- package/.agents/scripts/lib/orchestration/single-story-close/phases/confirm-merge.js +608 -152
- package/.agents/scripts/lib/orchestration/single-story-close/phases/options.js +72 -30
- package/.agents/scripts/lib/orchestration/single-story-close/phases/post-land.js +305 -0
- package/.agents/scripts/lib/orchestration/single-story-close/phases/pull-request.js +1 -1
- package/.agents/scripts/lib/orchestration/single-story-close/phases/review-block.js +12 -8
- package/.agents/scripts/lib/orchestration/single-story-close/phases/worktree-reap.js +37 -4
- package/.agents/scripts/lib/orchestration/single-story-close/phases/wrong-tree-guard.js +2 -2
- package/.agents/scripts/lib/orchestration/single-story-close/runner.js +264 -43
- package/.agents/scripts/lib/orchestration/single-story-lease-guard.js +1 -1
- package/.agents/scripts/lib/orchestration/story-close/format-autofix.js +10 -10
- package/.agents/scripts/lib/orchestration/story-close/phases/code-review.js +104 -279
- package/.agents/scripts/lib/orchestration/story-close/phases/local-lens-review.js +191 -0
- package/.agents/scripts/lib/orchestration/story-close/phases/review-core.js +120 -0
- package/.agents/scripts/lib/orchestration/story-deliver-terminal.js +360 -0
- package/.agents/scripts/lib/orchestration/story-follow-ups.js +75 -14
- package/.agents/scripts/lib/orchestration/story-init-remote.js +12 -8
- package/.agents/scripts/lib/orchestration/story-plan-state.js +14 -29
- package/.agents/scripts/lib/orchestration/task-body-validator.js +52 -7
- package/.agents/scripts/lib/orchestration/ticket-lease.js +27 -74
- package/.agents/scripts/lib/orchestration/ticket-validator-conflicts.js +119 -14
- package/.agents/scripts/lib/orchestration/ticket-validator-sizing.js +3 -4
- package/.agents/scripts/lib/orchestration/ticket-validator.js +121 -18
- package/.agents/scripts/lib/orchestration/ticketing/bulk.js +14 -47
- package/.agents/scripts/lib/orchestration/ticketing/reads.js +19 -32
- package/.agents/scripts/lib/orchestration/ticketing/transition.js +61 -1
- package/.agents/scripts/lib/orchestration/ticketing.js +0 -1
- package/.agents/scripts/lib/plan-phase-cleanup.js +12 -14
- package/.agents/scripts/lib/planning-corpus.js +12 -286
- package/.agents/scripts/lib/preflight-runner.js +2 -2
- package/.agents/scripts/lib/qa/qa-context-hydrator.js +5 -5
- package/.agents/scripts/lib/signals/index.js +4 -17
- package/.agents/scripts/lib/signals/read.js +35 -35
- package/.agents/scripts/lib/signals/schema.js +8 -11
- package/.agents/scripts/lib/signals/span-tree.js +7 -7
- package/.agents/scripts/lib/signals/write.js +0 -1
- package/.agents/scripts/lib/single-story/story-merged-notify.js +13 -2
- package/.agents/scripts/lib/skills/parse-skill.js +16 -3
- package/.agents/scripts/lib/story-adjacency.js +8 -7
- package/.agents/scripts/lib/story-body/story-body.js +81 -13
- package/.agents/scripts/lib/templates/decomposer-prompts.js +15 -16
- package/.agents/scripts/lib/test-env.js +14 -1
- package/.agents/scripts/lib/test-tiers.js +0 -3
- package/.agents/scripts/lib/ticket-body-sections.js +0 -14
- package/.agents/scripts/lib/validation-evidence.js +31 -59
- package/.agents/scripts/lib/wave-runner/live-probe.js +315 -0
- package/.agents/scripts/lib/wave-runner/ready-set.js +32 -6
- package/.agents/scripts/lib/worktree/lifecycle/pending-cleanup.js +1 -1
- package/.agents/scripts/lib/worktree/lifecycle/reap.js +68 -19
- package/.agents/scripts/lib/worktree/lifecycle-manager.js +1 -2
- package/.agents/scripts/plan-context.js +38 -7
- package/.agents/scripts/plan-critics.js +203 -0
- package/.agents/scripts/plan-persist.js +145 -35
- package/.agents/scripts/plan-run-epilogue.js +83 -38
- package/.agents/scripts/post-structured-comment.js +0 -38
- package/.agents/scripts/pr-watch-with-update.js +43 -22
- package/.agents/scripts/providers/github/compose.js +0 -1
- package/.agents/scripts/providers/github/errors.js +0 -19
- package/.agents/scripts/providers/github/issues.js +1 -11
- package/.agents/scripts/providers/github/mappers.js +5 -0
- package/.agents/scripts/providers/github/sub-issues.js +0 -47
- package/.agents/scripts/providers/github/tickets.js +33 -153
- package/.agents/scripts/providers/github.js +17 -6
- package/.agents/scripts/quality-preview.js +13 -6
- package/.agents/scripts/resolve-stories.js +236 -0
- package/.agents/scripts/run-coverage.js +4 -1
- package/.agents/scripts/run-lint.js +2 -2
- package/.agents/scripts/run-verify.js +31 -2
- package/.agents/scripts/signals-view.js +9 -10
- package/.agents/scripts/single-story-close.js +173 -18
- package/.agents/scripts/single-story-confirm-merge.js +288 -15
- package/.agents/scripts/single-story-init.js +6 -10
- package/.agents/scripts/stories-wave-tick.js +380 -53
- package/.agents/scripts/story-plan.js +3 -3
- package/.agents/scripts/update-ticket-state.js +8 -50
- package/.agents/skills/core/code-review-and-quality/SKILL.md +28 -450
- package/.agents/skills/core/code-review-and-quality/reference.md +458 -0
- package/.agents/skills/core/debugging-and-error-recovery/SKILL.md +22 -315
- package/.agents/skills/core/debugging-and-error-recovery/reference.md +323 -0
- package/.agents/skills/core/diagnose-friction/SKILL.md +14 -18
- package/.agents/skills/core/documentation-and-adrs/SKILL.md +25 -397
- package/.agents/skills/core/documentation-and-adrs/reference.md +403 -0
- package/.agents/skills/core/gates-and-baselines/SKILL.md +12 -12
- package/.agents/skills/core/idea-refinement/SKILL.md +3 -3
- package/.agents/skills/core/scope-triage/SKILL.md +3 -0
- package/.agents/skills/core/security-and-hardening/SKILL.md +22 -367
- package/.agents/skills/core/security-and-hardening/reference.md +375 -0
- package/.agents/skills/skills.index.json +2 -12
- package/.agents/skills/stack/qa/playwright-bdd/SKILL.md +2 -4
- package/.agents/skills/stack/qa/qa-explore-driving/SKILL.md +1 -1
- package/.agents/skills/stack/qa/qa-harness/SKILL.md +1 -3
- package/.agents/workflows/audit-architecture.md +3 -4
- package/.agents/workflows/audit-clean-code.md +4 -4
- package/.agents/workflows/audit-documentation.md +4 -5
- package/.agents/workflows/audit-lighthouse.md +8 -0
- package/.agents/workflows/audit-navigability.md +10 -0
- package/.agents/workflows/audit-performance.md +2 -3
- package/.agents/workflows/audit-quality.md +8 -9
- package/.agents/workflows/audit-security.md +1 -2
- package/.agents/workflows/audit-seo.md +10 -0
- package/.agents/workflows/audit-ux-ui.md +7 -0
- package/.agents/workflows/deliver.md +133 -45
- package/.agents/workflows/git-cleanup.md +2 -2
- package/.agents/workflows/git-deliver.md +1 -1
- package/.agents/workflows/helpers/acceptance-self-eval.md +34 -17
- package/.agents/workflows/helpers/code-quality-guardrails.md +15 -12
- package/.agents/workflows/helpers/code-review.md +14 -12
- package/.agents/workflows/helpers/deliver-story-reference.md +73 -32
- package/.agents/workflows/helpers/deliver-story.md +209 -118
- package/.agents/workflows/helpers/parallel-tooling.md +2 -2
- package/.agents/workflows/helpers/worktree-lifecycle.md +28 -32
- package/.agents/workflows/plan.md +239 -19
- package/.agents/workflows/qa-assist.md +6 -6
- package/.agents/workflows/qa-explore.md +3 -3
- package/.agents/workflows/qa-run.md +1 -5
- package/bin/mandrel.js +12 -1
- package/docs/CHANGELOG.md +62 -0
- package/lib/cli/registry.js +262 -19
- package/lib/cli/sync-agents.js +157 -0
- package/lib/cli/sync-commands.js +115 -6
- package/lib/cli/sync.js +168 -6
- package/lib/cli/update.js +105 -8
- package/lib/cli/version-helpers.js +131 -0
- package/lib/migrations/README.md +7 -5
- package/lib/migrations/index.js +17 -9
- package/lib/migrations/steps/2.1.0-retire-mi-drop-knobs.js +100 -0
- package/lib/migrations/steps/2.1.0-retire-verify-concurrency-cap.js +101 -0
- package/lib/migrations/steps/2.2.0-retire-epic-ac-tags.js +154 -0
- package/package.json +2 -2
- package/.agents/schemas/epic-perf-report.schema.json +0 -89
- package/.agents/schemas/lifecycle/acceptance.reconcile.failed.schema.json +0 -13
- package/.agents/schemas/lifecycle/acceptance.reconcile.ok.schema.json +0 -13
- package/.agents/schemas/lifecycle/acceptance.reconcile.skipped.schema.json +0 -13
- package/.agents/schemas/lifecycle/acceptance.reconcile.start.schema.json +0 -12
- package/.agents/schemas/lifecycle/acceptance.reconcile.waived.schema.json +0 -13
- package/.agents/schemas/lifecycle/epic.automerge.end.schema.json +0 -15
- package/.agents/schemas/lifecycle/epic.automerge.start.schema.json +0 -13
- package/.agents/schemas/lifecycle/epic.blocked.schema.json +0 -13
- package/.agents/schemas/lifecycle/epic.cleanup.end.schema.json +0 -12
- package/.agents/schemas/lifecycle/epic.cleanup.start.schema.json +0 -12
- package/.agents/schemas/lifecycle/epic.close.end.schema.json +0 -12
- package/.agents/schemas/lifecycle/epic.complete.schema.json +0 -13
- package/.agents/schemas/lifecycle/epic.finalize.end.schema.json +0 -13
- package/.agents/schemas/lifecycle/epic.finalize.start.schema.json +0 -12
- package/.agents/schemas/lifecycle/epic.merge.armed.schema.json +0 -13
- package/.agents/schemas/lifecycle/epic.merge.blocked.schema.json +0 -14
- package/.agents/schemas/lifecycle/epic.merge.confirmed.schema.json +0 -17
- package/.agents/schemas/lifecycle/epic.merge.ready.schema.json +0 -15
- package/.agents/schemas/lifecycle/epic.plan.end.schema.json +0 -18
- package/.agents/schemas/lifecycle/epic.plan.start.schema.json +0 -12
- package/.agents/schemas/lifecycle/epic.snapshot.end.schema.json +0 -16
- package/.agents/schemas/lifecycle/epic.snapshot.start.schema.json +0 -12
- package/.agents/schemas/lifecycle/epic.watch.end.schema.json +0 -29
- package/.agents/schemas/lifecycle/epic.watch.start.schema.json +0 -16
- package/.agents/schemas/lifecycle/story.heartbeat.schema.json +0 -20
- package/.agents/schemas/risk-verdict.schema.json +0 -53
- package/.agents/schemas/story-perf-summary.schema.json +0 -73
- package/.agents/scripts/analyze-execution.js +0 -444
- package/.agents/scripts/check-prepush-recovery.js +0 -90
- package/.agents/scripts/lib/git-merge-orchestrator.js +0 -261
- package/.agents/scripts/lib/observability/baseline-refresh-rate.js +0 -221
- package/.agents/scripts/lib/observability/hook-heartbeat.js +0 -187
- package/.agents/scripts/lib/observability/perf-aggregator.js +0 -813
- package/.agents/scripts/lib/observability/perf-report-readers.js +0 -328
- package/.agents/scripts/lib/observability/perf-report-render.js +0 -182
- package/.agents/scripts/lib/orchestration/audit-lens-routing.js +0 -128
- package/.agents/scripts/lib/orchestration/bookkeeping-outbox.js +0 -273
- package/.agents/scripts/lib/orchestration/error-journal.js +0 -139
- package/.agents/scripts/lib/orchestration/lifecycle/emit-story-heartbeat.js +0 -155
- package/.agents/scripts/lib/orchestration/lifecycle/ledger-diff.js +0 -140
- package/.agents/scripts/lib/orchestration/lifecycle/listeners/merge-watcher.js +0 -665
- package/.agents/scripts/lib/orchestration/plan-review-routing.js +0 -63
- package/.agents/scripts/lib/orchestration/planning/risk-verdict.js +0 -104
- package/.agents/scripts/lib/orchestration/planning-context-budget.js +0 -213
- package/.agents/scripts/lib/orchestration/planning-risk.js +0 -194
- package/.agents/scripts/lib/orchestration/post-merge/phases/branch-cleanup.js +0 -56
- package/.agents/scripts/lib/orchestration/post-merge/phases/dashboard-refresh.js +0 -21
- package/.agents/scripts/lib/orchestration/post-merge/phases/notification.js +0 -78
- package/.agents/scripts/lib/orchestration/post-merge/phases/temp-cleanup.js +0 -68
- package/.agents/scripts/lib/orchestration/post-merge/phases/ticket-closure.js +0 -118
- package/.agents/scripts/lib/orchestration/post-merge/phases/worktree-reap.js +0 -397
- package/.agents/scripts/lib/orchestration/preflight-cache.js +0 -187
- package/.agents/scripts/lib/orchestration/resolve-plan-run.js +0 -155
- package/.agents/scripts/lib/orchestration/retro-perf-heuristics.js +0 -275
- package/.agents/scripts/lib/orchestration/story-progress/story-run-progress-writer.js +0 -400
- package/.agents/scripts/lib/single-story/confirm-merge-follow-ups.js +0 -36
- package/.agents/scripts/resolve-plan-run.js +0 -117
- package/.agents/skills/core/analyze-execution/SKILL.md +0 -101
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* runtime-friction.js — derive friction signals from observables the
|
|
3
|
+
* runtime already has (Story #4578).
|
|
4
|
+
*
|
|
5
|
+
* ## Why this exists
|
|
6
|
+
*
|
|
7
|
+
* Friction telemetry was **opt-in**: a `friction` record existed only when
|
|
8
|
+
* an agent volunteered a `diagnose-friction.js --story <id> --cmd <...>`
|
|
9
|
+
* call. `plan-run-epilogue.js`'s `follow-up-rollup` then read an empty
|
|
10
|
+
* `signals.ndjson` and truthfully reported "No friction signals — nothing
|
|
11
|
+
* to follow up". A 7-Story run containing a mid-run git outage, a parked
|
|
12
|
+
* worker needing an operator resume, and a four-round acceptance critic
|
|
13
|
+
* produced a **zero-signal retro** — because the stream is least likely to
|
|
14
|
+
* fill exactly when a run is going badly and the agent is busy.
|
|
15
|
+
*
|
|
16
|
+
* This module closes that gap from the other side: the runtime emits
|
|
17
|
+
* friction from what it *already knows*, at the point it already knows it.
|
|
18
|
+
* No agent cooperation required.
|
|
19
|
+
*
|
|
20
|
+
* ## Not a second channel
|
|
21
|
+
*
|
|
22
|
+
* These records go through the **existing** `appendSignal` path, carry the
|
|
23
|
+
* **existing** `kind: 'friction'` shape, and land in the **same**
|
|
24
|
+
* `signals.ndjson` stream `diagnose-friction.js` writes and the roll-up
|
|
25
|
+
* reads. The only thing that is new is *who* decides to write.
|
|
26
|
+
*
|
|
27
|
+
* Deliberately distinct from {@link ../gates/friction.js}'s
|
|
28
|
+
* `emitFrictionSignal`, which early-returns unless **both** `storyId` and
|
|
29
|
+
* `epicId` are truthy. Every v2 Story is standalone (`epicId: null` — see
|
|
30
|
+
* `temp-paths.storyTempDir`'s standalone branch), and the roll-up reads the
|
|
31
|
+
* standalone stream via `forEachLine(null, sid, ...)`. Routing runtime
|
|
32
|
+
* friction through the gate helper would drop **every** record on the floor,
|
|
33
|
+
* silently, which is the bug this Story exists to fix.
|
|
34
|
+
*
|
|
35
|
+
* ## Robustness contract
|
|
36
|
+
*
|
|
37
|
+
* Observability MUST NOT halt the runner (`signals-writer.js`; the
|
|
38
|
+
* `docs/patterns.md` friction pattern). Every export here is best-effort:
|
|
39
|
+
* it swallows its own failures after a `Logger.warn` and resolves `false`.
|
|
40
|
+
* A missing signal is strictly preferable to a broken close.
|
|
41
|
+
*
|
|
42
|
+
* @module lib/observability/runtime-friction
|
|
43
|
+
*/
|
|
44
|
+
|
|
45
|
+
import crypto from 'node:crypto';
|
|
46
|
+
|
|
47
|
+
import { Logger } from '../Logger.js';
|
|
48
|
+
import { appendSignal } from './signals-writer.js';
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* The friction categories this module emits.
|
|
52
|
+
*
|
|
53
|
+
* Values are plain strings by design: `signal-event.schema.json` types
|
|
54
|
+
* `category` as a free-form `{ type: 'string', minLength: 1 }`, so no schema
|
|
55
|
+
* change is required to add one. They are frozen here so the emitters and
|
|
56
|
+
* their tests name the same literal instead of spreading it.
|
|
57
|
+
*
|
|
58
|
+
* Category choice is load-bearing for the retro: `retro-proposals.js`
|
|
59
|
+
* aggregates **by exact category string**, so these are deliberately coarse
|
|
60
|
+
* — one bucket per failure mode — rather than per-incident. Two Stories that
|
|
61
|
+
* hit the same wall in one run aggregate to `occurrences: 2` and route as a
|
|
62
|
+
* real proposal instead of being discarded as two unrelated singletons.
|
|
63
|
+
*/
|
|
64
|
+
export const RUNTIME_FRICTION_CATEGORIES = Object.freeze({
|
|
65
|
+
/** A Story was parked at `agent::blocked` — the HITL pause (§ 1.J). */
|
|
66
|
+
STORY_BLOCKED: 'story-blocked',
|
|
67
|
+
/** A close run ended on a `failed` terminal (non-zero exit). */
|
|
68
|
+
CLOSE_FAILED: 'close-failed',
|
|
69
|
+
/** A bounded merge wait expired with the PR still in flight. */
|
|
70
|
+
MERGE_WAIT_EXHAUSTED: 'merge-wait-exhausted',
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
/** Cap on free-form reason text copied into a signal's `details`. */
|
|
74
|
+
const REASON_PREVIEW_LIMIT = 500;
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* @param {unknown} value
|
|
78
|
+
* @returns {string}
|
|
79
|
+
*/
|
|
80
|
+
function preview(value) {
|
|
81
|
+
return String(value ?? '').slice(0, REASON_PREVIEW_LIMIT);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* @param {unknown} value
|
|
86
|
+
* @returns {number|null}
|
|
87
|
+
*/
|
|
88
|
+
function positiveIntOrNull(value) {
|
|
89
|
+
const n = Number(value);
|
|
90
|
+
return Number.isInteger(n) && n > 0 ? n : null;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Append one runtime-derived `friction` record to the Story's signals
|
|
95
|
+
* stream. Best-effort: never throws, never rejects.
|
|
96
|
+
*
|
|
97
|
+
* `epicId` defaults to `null` (the standalone-Story stream) because that is
|
|
98
|
+
* where v2 Stories live and where the roll-up reads.
|
|
99
|
+
*
|
|
100
|
+
* @param {object} args
|
|
101
|
+
* @param {number} args.storyId
|
|
102
|
+
* @param {number|null} [args.epicId]
|
|
103
|
+
* @param {string} args.category One of {@link RUNTIME_FRICTION_CATEGORIES}.
|
|
104
|
+
* @param {string} args.tool Emitting surface, for `emitter.tool`.
|
|
105
|
+
* @param {object} [args.details] Kind-specific payload (always an object).
|
|
106
|
+
* @param {object} [args.config] Resolved config (for `tempRoot`).
|
|
107
|
+
* @returns {Promise<boolean>} true when a record was appended.
|
|
108
|
+
*/
|
|
109
|
+
export async function emitRuntimeFriction({
|
|
110
|
+
storyId,
|
|
111
|
+
epicId = null,
|
|
112
|
+
category,
|
|
113
|
+
tool,
|
|
114
|
+
details = {},
|
|
115
|
+
config,
|
|
116
|
+
} = {}) {
|
|
117
|
+
const sid = positiveIntOrNull(storyId);
|
|
118
|
+
if (sid === null) {
|
|
119
|
+
// No Story context → no stream to write to. Not an error: some close
|
|
120
|
+
// paths (a usage error before the id is parsed) genuinely have none.
|
|
121
|
+
return false;
|
|
122
|
+
}
|
|
123
|
+
if (typeof category !== 'string' || category.trim() === '') {
|
|
124
|
+
Logger.warn('[runtime-friction] refusing to emit a category-less signal');
|
|
125
|
+
return false;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const signal = {
|
|
129
|
+
kind: 'friction',
|
|
130
|
+
eventId: crypto.randomUUID(),
|
|
131
|
+
ts: new Date().toISOString(),
|
|
132
|
+
epicId: positiveIntOrNull(epicId),
|
|
133
|
+
storyId: sid,
|
|
134
|
+
// 2-tier hierarchy (Epic #3163): no Task tier. Retained for schema
|
|
135
|
+
// compatibility and always null — mirrors diagnose-friction.js.
|
|
136
|
+
taskId: null,
|
|
137
|
+
category: category.trim(),
|
|
138
|
+
emitter: { tool: tool || 'runtime-friction' },
|
|
139
|
+
details: details && typeof details === 'object' ? details : {},
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
try {
|
|
143
|
+
return await appendSignal({
|
|
144
|
+
epicId: signal.epicId,
|
|
145
|
+
storyId: sid,
|
|
146
|
+
signal,
|
|
147
|
+
config,
|
|
148
|
+
});
|
|
149
|
+
} catch (err) {
|
|
150
|
+
// `appendSignal` already swallows its own I/O failures; this catch is
|
|
151
|
+
// defense in depth so a surprise (a poisoned config, a throwing
|
|
152
|
+
// validator) still cannot take down the path being observed.
|
|
153
|
+
Logger.warn(
|
|
154
|
+
`[runtime-friction] append failed for Story #${sid} (${category}): ${
|
|
155
|
+
err instanceof Error ? err.message : String(err)
|
|
156
|
+
}`,
|
|
157
|
+
);
|
|
158
|
+
return false;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Decide whether a `story-deliver-terminal` envelope is worth a friction
|
|
164
|
+
* record, and describe it. **Pure** — no I/O — so the (interesting) policy
|
|
165
|
+
* is unit-testable without touching disk.
|
|
166
|
+
*
|
|
167
|
+
* The partition is deliberate; each observable is emitted from exactly ONE
|
|
168
|
+
* place, so one incident never double-counts into `occurrences: 2` and
|
|
169
|
+
* fabricates a filed proposal out of a single event:
|
|
170
|
+
*
|
|
171
|
+
* - `blocked` → **null here.** Every blocked terminal flips the Story to
|
|
172
|
+
* `agent::blocked` (`confirm-merge.js`, both the `merge.unlanded` and
|
|
173
|
+
* `merge.flip-failed` paths), and that transition is instrumented at the
|
|
174
|
+
* canonical mutator (`ticketing/transition.js`). Emitting here as well
|
|
175
|
+
* would count the same block twice.
|
|
176
|
+
* - `landed` → null. Nothing happened worth a retro.
|
|
177
|
+
* - `failed` → friction. A close that ended non-zero.
|
|
178
|
+
* - `pending` → friction **only when a `waitBudget` was exhausted**. That
|
|
179
|
+
* is the parked worker from the report: a bounded wait expired with the
|
|
180
|
+
* PR in flight and a human must resume it. A `pending` with **no**
|
|
181
|
+
* `waitBudget` is the `--no-wait-merge` / operator-merge path, where the
|
|
182
|
+
* human deliberately owns the land and nothing is broken — flagging it
|
|
183
|
+
* would train operators to ignore the channel.
|
|
184
|
+
*
|
|
185
|
+
* Deliberately **not exported**: it is this module's internal policy, and
|
|
186
|
+
* `emitTerminalFriction` is the contract callers (and tests) exercise. An
|
|
187
|
+
* export solely for testability would be production-dead code — the
|
|
188
|
+
* `--production` dead-exports ratchet exists to catch exactly that.
|
|
189
|
+
*
|
|
190
|
+
* @param {object} envelope A `story-deliver-terminal` envelope.
|
|
191
|
+
* @returns {{ category: string, details: object }|null}
|
|
192
|
+
*/
|
|
193
|
+
function frictionForTerminal(envelope) {
|
|
194
|
+
if (!envelope || typeof envelope !== 'object') return null;
|
|
195
|
+
const { status, phase, waitBudget, failure, pr } = envelope;
|
|
196
|
+
|
|
197
|
+
if (status === 'failed') {
|
|
198
|
+
return {
|
|
199
|
+
category: RUNTIME_FRICTION_CATEGORIES.CLOSE_FAILED,
|
|
200
|
+
details: {
|
|
201
|
+
phase: phase ?? null,
|
|
202
|
+
reason: preview(failure?.reason),
|
|
203
|
+
},
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
if (status === 'pending' && waitBudget) {
|
|
208
|
+
return {
|
|
209
|
+
category: RUNTIME_FRICTION_CATEGORIES.MERGE_WAIT_EXHAUSTED,
|
|
210
|
+
details: {
|
|
211
|
+
phase: phase ?? null,
|
|
212
|
+
prNumber: pr?.number ?? null,
|
|
213
|
+
checksStatus: pr?.checksStatus ?? null,
|
|
214
|
+
waitedSeconds: waitBudget.waitedSeconds ?? null,
|
|
215
|
+
cumulativeSeconds: waitBudget.cumulativeSeconds ?? null,
|
|
216
|
+
maxBudgetSeconds: waitBudget.maxBudgetSeconds ?? null,
|
|
217
|
+
},
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
return null;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Emit the friction record (if any) implied by a terminal envelope.
|
|
226
|
+
* Best-effort; never throws.
|
|
227
|
+
*
|
|
228
|
+
* @param {object} args
|
|
229
|
+
* @param {object} args.envelope
|
|
230
|
+
* @param {object} [args.config]
|
|
231
|
+
* @returns {Promise<boolean>} true when a record was appended.
|
|
232
|
+
*/
|
|
233
|
+
export async function emitTerminalFriction({ envelope, config } = {}) {
|
|
234
|
+
const verdict = frictionForTerminal(envelope);
|
|
235
|
+
if (!verdict) return false;
|
|
236
|
+
return emitRuntimeFriction({
|
|
237
|
+
storyId: envelope?.storyId,
|
|
238
|
+
category: verdict.category,
|
|
239
|
+
tool: 'single-story-close',
|
|
240
|
+
details: verdict.details,
|
|
241
|
+
config,
|
|
242
|
+
});
|
|
243
|
+
}
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
* other draft-07 validators (see `tests/schemas/signal-schemas.test.js`).
|
|
19
19
|
*
|
|
20
20
|
* A per-Epic reject tally is persisted under the Epic temp tree
|
|
21
|
-
* (`temp/
|
|
21
|
+
* (`temp/run-<eid>/signal-rejects.json`) so a cross-process reader (the
|
|
22
22
|
* loop-health check, a follow-on Story) can surface how many records were
|
|
23
23
|
* dropped. The tally is read-modify-written best-effort; a lost increment
|
|
24
24
|
* under a write race is acceptable for a diagnostic counter.
|
|
@@ -32,7 +32,7 @@ import { fileURLToPath } from 'node:url';
|
|
|
32
32
|
import Ajv from 'ajv';
|
|
33
33
|
import addFormats from 'ajv-formats';
|
|
34
34
|
|
|
35
|
-
import {
|
|
35
|
+
import { runArtifactPath } from '../config/temp-paths.js';
|
|
36
36
|
import { Logger } from '../Logger.js';
|
|
37
37
|
|
|
38
38
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
@@ -141,7 +141,7 @@ export async function recordSignalReject({ epicId, config, field }) {
|
|
|
141
141
|
if (!Number.isInteger(epicId) || epicId <= 0) return null;
|
|
142
142
|
let target;
|
|
143
143
|
try {
|
|
144
|
-
target =
|
|
144
|
+
target = runArtifactPath(epicId, REJECT_TALLY_BASENAME, config);
|
|
145
145
|
} catch {
|
|
146
146
|
return null;
|
|
147
147
|
}
|
|
@@ -190,7 +190,7 @@ export async function readSignalRejectCount({ epicId, config }) {
|
|
|
190
190
|
if (!Number.isInteger(epicId) || epicId <= 0) return 0;
|
|
191
191
|
let target;
|
|
192
192
|
try {
|
|
193
|
-
target =
|
|
193
|
+
target = runArtifactPath(epicId, REJECT_TALLY_BASENAME, config);
|
|
194
194
|
} catch {
|
|
195
195
|
return 0;
|
|
196
196
|
}
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Append-only signals/trace writer (Epic #1030 Story #1041).
|
|
3
3
|
*
|
|
4
4
|
* Centralizes the per-(epic, story) NDJSON streams under
|
|
5
|
-
* `temp/
|
|
5
|
+
* `temp/run-<id>/stories/story-<sid>/signals.ndjson` (and a sibling
|
|
6
6
|
* `traces.ndjson` for trace-shaped records). Detector modules and the
|
|
7
7
|
* runtime trace hook all funnel through this writer so the on-disk
|
|
8
8
|
* shape stays under one schema and one set of robustness guarantees.
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
* fire from inside per-Story sub-agents that may exit abruptly, and
|
|
19
19
|
* a buffered tail would silently disappear on `process.exit`.
|
|
20
20
|
* - **Lazy directory creation.** The first write to a fresh Story
|
|
21
|
-
* creates `temp/
|
|
21
|
+
* creates `temp/run-<id>/stories/story-<sid>/` via `fs.mkdir(..., { recursive: true })`.
|
|
22
22
|
* `epicId` / `storyId` are required positive integers — the
|
|
23
23
|
* `temp-paths.js` helpers assert this before we touch the disk.
|
|
24
24
|
*
|
|
@@ -36,17 +36,12 @@ import fs from 'node:fs/promises';
|
|
|
36
36
|
import path from 'node:path';
|
|
37
37
|
import { createInterface } from 'node:readline';
|
|
38
38
|
|
|
39
|
-
import {
|
|
40
|
-
epicArtifactPath,
|
|
41
|
-
signalsFile,
|
|
42
|
-
storyTempDir,
|
|
43
|
-
} from '../config/temp-paths.js';
|
|
39
|
+
import { signalsFile, storyTempDir } from '../config/temp-paths.js';
|
|
44
40
|
import { Logger } from '../Logger.js';
|
|
45
41
|
import { recordSignalReject, validateSignal } from './signal-validator.js';
|
|
46
42
|
import { classifyPathSource } from './source-classifier.js';
|
|
47
43
|
|
|
48
44
|
const TRACES_BASENAME = 'traces.ndjson';
|
|
49
|
-
const EPIC_SIGNALS_BASENAME = 'signals.ndjson';
|
|
50
45
|
|
|
51
46
|
/**
|
|
52
47
|
* Async traces-file path (kept private — consumers thread through
|
|
@@ -167,7 +162,7 @@ async function appendOne(targetPath, record) {
|
|
|
167
162
|
}
|
|
168
163
|
|
|
169
164
|
/**
|
|
170
|
-
* Append one signal record to `temp/
|
|
165
|
+
* Append one signal record to `temp/run-<id>/stories/story-<sid>/signals.ndjson`.
|
|
171
166
|
*
|
|
172
167
|
* The `signal` is written verbatim — callers (detectors) own its shape
|
|
173
168
|
* (kind, severity, message, etc.). The writer adds nothing. Errors are
|
|
@@ -200,42 +195,7 @@ export async function appendSignal(args) {
|
|
|
200
195
|
}
|
|
201
196
|
|
|
202
197
|
/**
|
|
203
|
-
* Append one
|
|
204
|
-
* `temp/epic-<eid>/signals.ndjson` — used for Epic-scoped signals not tied
|
|
205
|
-
* to an individual Story: the wave-window forensics kinds (`wave-start`,
|
|
206
|
-
* `wave-complete`), notification-derived anchors (`state-transition`,
|
|
207
|
-
* `notification.emitted`), and Epic-level `friction` (e.g.
|
|
208
|
-
* `lifecycle-emit.js`). The retired write-only kinds `wave-tick` /
|
|
209
|
-
* `epic-complete` are gone (Story #3909) and are NOT valid here.
|
|
210
|
-
*
|
|
211
|
-
* @param {{ epicId: number, signal: unknown, config?: object }} args
|
|
212
|
-
* @returns {Promise<boolean>}
|
|
213
|
-
*/
|
|
214
|
-
export async function appendEpicSignal(args) {
|
|
215
|
-
const { epicId, signal, config } = args ?? {};
|
|
216
|
-
let target;
|
|
217
|
-
try {
|
|
218
|
-
target = epicArtifactPath(epicId, EPIC_SIGNALS_BASENAME, config);
|
|
219
|
-
} catch (err) {
|
|
220
|
-
Logger.warn(
|
|
221
|
-
`signals-writer: invalid epicId for appendEpicSignal: ${
|
|
222
|
-
err instanceof Error ? err.message : String(err)
|
|
223
|
-
}`,
|
|
224
|
-
);
|
|
225
|
-
return false;
|
|
226
|
-
}
|
|
227
|
-
const tagged = tagSignalSource(signal);
|
|
228
|
-
const ok = await validateOrDrop(tagged, {
|
|
229
|
-
epicId: Number.isInteger(epicId) ? epicId : null,
|
|
230
|
-
config,
|
|
231
|
-
label: 'epic signal',
|
|
232
|
-
});
|
|
233
|
-
if (!ok) return false;
|
|
234
|
-
return appendOne(target, tagged);
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
/**
|
|
238
|
-
* Append one trace record to `temp/epic-<eid>/stories/story-<sid>/traces.ndjson`.
|
|
198
|
+
* Append one trace record to `temp/run-<id>/stories/story-<sid>/traces.ndjson`.
|
|
239
199
|
* Same robustness contract as `appendSignal` — never throws.
|
|
240
200
|
*
|
|
241
201
|
* @param {{ epicId: number, storyId: number, trace: unknown, config?: object }} args
|
|
@@ -270,7 +230,7 @@ export async function appendTrace(args) {
|
|
|
270
230
|
* lines are skipped with a `Logger.warn`. A missing file resolves with
|
|
271
231
|
* `missing: true` rather than throwing.
|
|
272
232
|
*
|
|
273
|
-
* Shared spine for `forEachLine`
|
|
233
|
+
* Shared spine for `forEachLine`
|
|
274
234
|
* (per-Epic stream) so the two readers cannot drift in their
|
|
275
235
|
* malformed-line / missing-file / cb-throw handling.
|
|
276
236
|
*
|
|
@@ -361,39 +321,3 @@ export async function forEachLine(epicId, storyId, cb, config) {
|
|
|
361
321
|
|
|
362
322
|
return forEachLineIn(target, cb, 'forEachLine');
|
|
363
323
|
}
|
|
364
|
-
|
|
365
|
-
/**
|
|
366
|
-
* Stream the per-Epic `signals.ndjson` line by line, invoking
|
|
367
|
-
* `cb(parsed, lineNumber)` for each successfully parsed JSON line. This is
|
|
368
|
-
* the read side of `appendEpicSignal` — the wave-lifecycle stream at
|
|
369
|
-
* `temp/epic-<eid>/signals.ndjson` written by `appendEpicSignal` callers
|
|
370
|
-
* (e.g. `lifecycle-emit`). A missing file resolves with `missing: true`
|
|
371
|
-
* rather than throwing, mirroring the per-Story `forEachLine` contract so
|
|
372
|
-
* the retro's unified counts scan can fold the Epic stream in alongside the
|
|
373
|
-
* Story streams without a special-cased absence branch.
|
|
374
|
-
*
|
|
375
|
-
* @param {number} epicId
|
|
376
|
-
* @param {(parsed: unknown, lineNumber: number) => unknown | Promise<unknown>} cb
|
|
377
|
-
* @param {object} [config]
|
|
378
|
-
* @returns {Promise<{ linesRead: number, linesParsed: number, missing: boolean }>}
|
|
379
|
-
*/
|
|
380
|
-
export async function forEachEpicLine(epicId, cb, config) {
|
|
381
|
-
if (typeof cb !== 'function') {
|
|
382
|
-
Logger.warn('signals-writer: forEachEpicLine called without a callback');
|
|
383
|
-
return { linesRead: 0, linesParsed: 0, missing: false };
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
let target;
|
|
387
|
-
try {
|
|
388
|
-
target = epicArtifactPath(epicId, EPIC_SIGNALS_BASENAME, config);
|
|
389
|
-
} catch (err) {
|
|
390
|
-
Logger.warn(
|
|
391
|
-
`signals-writer: invalid epicId for forEachEpicLine: ${
|
|
392
|
-
err instanceof Error ? err.message : String(err)
|
|
393
|
-
}`,
|
|
394
|
-
);
|
|
395
|
-
return { linesRead: 0, linesParsed: 0, missing: false };
|
|
396
|
-
}
|
|
397
|
-
|
|
398
|
-
return forEachLineIn(target, cb, 'forEachEpicLine');
|
|
399
|
-
}
|
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
* `"consumer"` (the host project that consumes the framework via the
|
|
5
5
|
* materialized `.agents/` directory).
|
|
6
6
|
*
|
|
7
|
-
* Used by `signals-writer.js#appendSignal`
|
|
8
|
-
* record in `temp/
|
|
7
|
+
* Used by `signals-writer.js#appendSignal` so every
|
|
8
|
+
* record in `temp/run-<id>/stories/story-<sid>/signals.ndjson` carries an
|
|
9
9
|
* authoritative `source` field, allowing downstream retro consumers to
|
|
10
10
|
* route framework signals back to mandrel and keep consumer signals in
|
|
11
11
|
* the host project (Epic #2547 / Story #2553).
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* PostToolUse hook entries. Resolves the active Epic + Story from
|
|
6
6
|
* environment variables (`CC_EPIC_ID` / `CC_STORY_ID`), pairs Pre/Post
|
|
7
7
|
* tool-call events, and appends one `kind:"trace"` NDJSON line per tool
|
|
8
|
-
* call to `temp/
|
|
8
|
+
* call to `temp/run-<id>/stories/story-<sid>/traces.ndjson` via the
|
|
9
9
|
* `signals-writer.appendTrace` helper.
|
|
10
10
|
*
|
|
11
11
|
* Robustness contract (Tech Spec #1032 §observability + §security):
|
|
@@ -48,7 +48,6 @@
|
|
|
48
48
|
|
|
49
49
|
import { createHash } from 'node:crypto';
|
|
50
50
|
|
|
51
|
-
import { emitHeartbeatFromHook } from './hook-heartbeat.js';
|
|
52
51
|
import { appendTrace } from './signals-writer.js';
|
|
53
52
|
|
|
54
53
|
/**
|
|
@@ -388,19 +387,10 @@ export async function main(event) {
|
|
|
388
387
|
const phase = event.hook_event_name;
|
|
389
388
|
if (phase === 'PreToolUse') {
|
|
390
389
|
// Pre-pairing only matters for the trace-line duration, which only
|
|
391
|
-
// the Story-scoped trace path records
|
|
392
|
-
// delivery) needs no Pre.
|
|
390
|
+
// the Story-scoped trace path records.
|
|
393
391
|
if (active) handlePre(event);
|
|
394
392
|
} else if (phase === 'PostToolUse') {
|
|
395
393
|
if (active) await handlePost(event, active);
|
|
396
|
-
// Heartbeat OFF the token stream (Epic #4476 M5). A throttled
|
|
397
|
-
// story.heartbeat / slice.heartbeat, keyed off the same active-Story /
|
|
398
|
-
// active-slice env vars, so the §2e Idle Watchdog's forward-progress
|
|
399
|
-
// signal is a free byproduct of ANY tool call — no dedicated
|
|
400
|
-
// bookkeeping LLM turn. Best-effort and self-guarded; resolves its own
|
|
401
|
-
// target (fires for slice context even when `active` is null because
|
|
402
|
-
// there is no CC_STORY_ID under single delivery).
|
|
403
|
-
emitHeartbeatFromHook();
|
|
404
394
|
}
|
|
405
395
|
// Any other phase is silently ignored — the hook is registered for
|
|
406
396
|
// Pre/Post only; receiving anything else is a configuration error
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
* hard-clamped to `[1, 8]`), and the executor spawns ONE maker-blind
|
|
16
16
|
* fresh-context `Agent` critic per cluster. The cluster count is therefore
|
|
17
17
|
* `ceil(totalACs / clusterCeiling)` — a deterministic fan-out width the
|
|
18
|
-
*
|
|
18
|
+
* consuming executor and its isolation test both key off.
|
|
19
19
|
*
|
|
20
20
|
* Pure and total — inputs in, clusters out. No I/O, no throws on malformed
|
|
21
21
|
* input (a non-array / empty AC set yields zero clusters — the executor then
|
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
|
|
42
42
|
import { readFileSync } from 'node:fs';
|
|
43
43
|
|
|
44
|
-
import {
|
|
44
|
+
import { runArtifactPath, signalsFile } from '../config/temp-paths.js';
|
|
45
45
|
|
|
46
46
|
/** Epic-level signals stream basename (mirrors signals-writer). */
|
|
47
47
|
const EPIC_SIGNALS_BASENAME = 'signals.ndjson';
|
|
@@ -231,7 +231,7 @@ export function deriveAcceptanceEvalRound({
|
|
|
231
231
|
readFile = (p) => readFileSync(p, 'utf8'),
|
|
232
232
|
signalsPathResolver = signalsFile,
|
|
233
233
|
epicSignalsPathResolver = (eid, cfg) =>
|
|
234
|
-
|
|
234
|
+
runArtifactPath(eid, EPIC_SIGNALS_BASENAME, cfg),
|
|
235
235
|
}) {
|
|
236
236
|
// Epic #4475 (M4-B): single-delivery critics score AC clusters, not
|
|
237
237
|
// Stories. When `clusterId` is supplied the round is counted per cluster
|
|
@@ -1,21 +1,29 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* lib/orchestration/ceremony-routing.js — ceremony-profile +
|
|
2
|
+
* lib/orchestration/ceremony-routing.js — ceremony-profile + derived-level
|
|
3
3
|
* acceptance ceremony resolver.
|
|
4
4
|
*
|
|
5
|
-
* The sibling of `review-depth.js
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
5
|
+
* The sibling of `review-depth.js#resolveDepth`: it folds the operator ceremony
|
|
6
|
+
* profile and the **derived** change level into a per-cluster ceremony decision
|
|
7
|
+
* for the single-delivery acceptance critic — **fresh-context spawn** vs the
|
|
8
|
+
* contract-identical **inline** critic. It does NOT invent a new risk score and
|
|
9
|
+
* it does NOT own clustering.
|
|
10
|
+
*
|
|
11
|
+
* ## One derived source, two decisions (Story #4542)
|
|
12
|
+
*
|
|
13
|
+
* `derivedLevel` comes from `review-depth.js#deriveChangeLevel` — the same call
|
|
14
|
+
* that feeds review depth — so both ceremony decisions read one observable
|
|
15
|
+
* signal: does the change set touch a sensitive path registered in
|
|
16
|
+
* `audit-rules.json`? Previously this consumed the planner's own risk verdict,
|
|
17
|
+
* which meant a confident all-low self-assertion bought *less* independent
|
|
18
|
+
* checking than authoring nothing at all. A derived level cannot be talked down.
|
|
11
19
|
*
|
|
12
20
|
* ## Ceremony profiles (`delivery.routing.ceremonyProfile`)
|
|
13
21
|
*
|
|
14
22
|
* - `minimal` — always `inline` (skip fresh critic + sampling floor).
|
|
15
23
|
* Use for tiny N=1 Stories the operator trusts.
|
|
16
|
-
* - `standard` —
|
|
17
|
-
*
|
|
18
|
-
* - `strict` — always `fresh` regardless of
|
|
24
|
+
* - `standard` — level-routed (default). Low → inline (+ sampling floor);
|
|
25
|
+
* high → fresh.
|
|
26
|
+
* - `strict` — always `fresh` regardless of the derived level.
|
|
19
27
|
*
|
|
20
28
|
* ## The load-bearing invariant (M4-B acceptance floor — DO NOT VIOLATE)
|
|
21
29
|
*
|
|
@@ -29,23 +37,22 @@
|
|
|
29
37
|
*
|
|
30
38
|
* ## Tier rules (per cluster, `standard` profile)
|
|
31
39
|
*
|
|
32
|
-
* - `high`
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
* - `low` risk → `inline` (the contract-identical inline critic),
|
|
40
|
+
* - `high` level → `fresh` (a sensitive path was touched — a
|
|
41
|
+
* fresh-context maker-blind spawn).
|
|
42
|
+
* - `low` level → `inline` (the contract-identical inline critic),
|
|
36
43
|
* UNLESS the maker-checker sampling floor
|
|
37
44
|
* selects this cluster → `fresh`.
|
|
38
|
-
* - missing / unknown → `fresh` (fail-safe:
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
45
|
+
* - missing / unknown → `fresh` (fail-safe: the diff could not be
|
|
46
|
+
* enumerated, so there is no evidence the
|
|
47
|
+
* change is unremarkable; treat it as
|
|
48
|
+
* needing the full fresh-context ceremony,
|
|
49
|
+
* exactly as `resolveDepth` degrades to
|
|
50
|
+
* `standard` on the same signal).
|
|
44
51
|
*
|
|
45
52
|
* ## Maker-checker sampling floor
|
|
46
53
|
*
|
|
47
|
-
* Even at `low`
|
|
48
|
-
* (`freshCriticSampleRate`, default 0.2) is forced `fresh` so low
|
|
54
|
+
* Even at a `low` derived level under `standard`, a fraction of clusters
|
|
55
|
+
* (`freshCriticSampleRate`, default 0.2) is forced `fresh` so a low level never
|
|
49
56
|
* means zero independent checking. The selection is **deterministic** in the
|
|
50
57
|
* cluster index (a fixed stride), so it is stable across re-runs and —
|
|
51
58
|
* critically — never changes the cluster count: it only re-labels which of
|
|
@@ -56,7 +63,7 @@
|
|
|
56
63
|
* `undefined` / malformed inputs degrade to `fresh` + `full` ceremony.
|
|
57
64
|
*
|
|
58
65
|
* @typedef {'fresh'|'inline'} CeremonyMode
|
|
59
|
-
* @typedef {'
|
|
66
|
+
* @typedef {import('./review-depth.js').ChangeLevel} ChangeLevel
|
|
60
67
|
* @typedef {'minimal'|'standard'|'strict'} CeremonyProfile
|
|
61
68
|
*/
|
|
62
69
|
|
|
@@ -112,11 +119,11 @@ export function sampledFresh(clusterIndex, rate) {
|
|
|
112
119
|
|
|
113
120
|
/**
|
|
114
121
|
* Resolve the acceptance ceremony for one cluster from the ceremony profile,
|
|
115
|
-
*
|
|
116
|
-
* header for the tier rules and the untouchable cluster-count invariant.
|
|
122
|
+
* the derived change level, and the maker-checker sampling floor. See the
|
|
123
|
+
* module header for the tier rules and the untouchable cluster-count invariant.
|
|
117
124
|
*
|
|
118
125
|
* @param {{
|
|
119
|
-
*
|
|
126
|
+
* derivedLevel?: (ChangeLevel|string|null|undefined),
|
|
120
127
|
* clusterIndex?: (number|null|undefined),
|
|
121
128
|
* freshCriticSampleRate?: (number|null|undefined),
|
|
122
129
|
* ceremonyProfile?: (CeremonyProfile|string|null|undefined),
|
|
@@ -129,8 +136,8 @@ export function sampledFresh(clusterIndex, rate) {
|
|
|
129
136
|
* }}
|
|
130
137
|
*/
|
|
131
138
|
export function resolveCeremonyForRisk(input = {}) {
|
|
132
|
-
const
|
|
133
|
-
input && typeof input === 'object' ? input.
|
|
139
|
+
const derivedLevel =
|
|
140
|
+
input && typeof input === 'object' ? input.derivedLevel : undefined;
|
|
134
141
|
const clusterIndex =
|
|
135
142
|
input && typeof input === 'object' ? input.clusterIndex : undefined;
|
|
136
143
|
const rate =
|
|
@@ -158,46 +165,37 @@ export function resolveCeremonyForRisk(input = {}) {
|
|
|
158
165
|
};
|
|
159
166
|
}
|
|
160
167
|
|
|
161
|
-
if (
|
|
162
|
-
return {
|
|
163
|
-
mode: 'fresh',
|
|
164
|
-
reason: 'high-risk: fresh-context critic',
|
|
165
|
-
sampled: false,
|
|
166
|
-
profile,
|
|
167
|
-
};
|
|
168
|
-
}
|
|
169
|
-
if (overallLevel === 'medium') {
|
|
168
|
+
if (derivedLevel === 'high') {
|
|
170
169
|
return {
|
|
171
170
|
mode: 'fresh',
|
|
172
|
-
reason: '
|
|
171
|
+
reason: 'sensitive path touched: fresh-context critic',
|
|
173
172
|
sampled: false,
|
|
174
173
|
profile,
|
|
175
174
|
};
|
|
176
175
|
}
|
|
177
|
-
if (
|
|
176
|
+
if (derivedLevel === 'low') {
|
|
178
177
|
if (sampledFresh(clusterIndex, rate)) {
|
|
179
178
|
return {
|
|
180
179
|
mode: 'fresh',
|
|
181
180
|
reason:
|
|
182
|
-
'low-
|
|
181
|
+
'low-level cluster forced fresh by the maker-checker sampling floor',
|
|
183
182
|
sampled: true,
|
|
184
183
|
profile,
|
|
185
184
|
};
|
|
186
185
|
}
|
|
187
186
|
return {
|
|
188
187
|
mode: 'inline',
|
|
189
|
-
reason: '
|
|
188
|
+
reason: 'no sensitive path touched: contract-identical inline critic',
|
|
190
189
|
sampled: false,
|
|
191
190
|
profile,
|
|
192
191
|
};
|
|
193
192
|
}
|
|
194
|
-
// Missing / unknown / malformed
|
|
195
|
-
// matching how
|
|
196
|
-
// unjudged Story.
|
|
193
|
+
// Missing / unknown / malformed level → fail-safe fresh + full ceremony,
|
|
194
|
+
// matching how resolveDepth degrades to `standard` on the same signal.
|
|
197
195
|
return {
|
|
198
196
|
mode: 'fresh',
|
|
199
197
|
reason:
|
|
200
|
-
'
|
|
198
|
+
'change level underivable: fail-safe fresh-context critic + full ceremony',
|
|
201
199
|
sampled: false,
|
|
202
200
|
profile,
|
|
203
201
|
};
|