@dzhechkov/harness-core 0.8.10 → 0.8.20

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.
Files changed (251) hide show
  1. package/.dz-manifest.json +490 -170
  2. package/LICENSE +13 -0
  3. package/README.md +420 -6
  4. package/dist/agents-policy.d.ts +15 -1
  5. package/dist/agents-policy.d.ts.map +1 -1
  6. package/dist/agents-policy.js +27 -1
  7. package/dist/agents-policy.js.map +1 -1
  8. package/dist/amendment-trace.d.ts +72 -0
  9. package/dist/amendment-trace.d.ts.map +1 -1
  10. package/dist/amendment-trace.js +385 -17
  11. package/dist/amendment-trace.js.map +1 -1
  12. package/dist/backlog-public.d.ts +153 -0
  13. package/dist/backlog-public.d.ts.map +1 -0
  14. package/dist/backlog-public.js +415 -0
  15. package/dist/backlog-public.js.map +1 -0
  16. package/dist/backlog-transitions.d.ts +48 -0
  17. package/dist/backlog-transitions.d.ts.map +1 -0
  18. package/dist/backlog-transitions.js +64 -0
  19. package/dist/backlog-transitions.js.map +1 -0
  20. package/dist/backlog.d.ts.map +1 -1
  21. package/dist/backlog.js +13 -0
  22. package/dist/backlog.js.map +1 -1
  23. package/dist/claim-check.d.ts.map +1 -1
  24. package/dist/claim-check.js +24 -2
  25. package/dist/claim-check.js.map +1 -1
  26. package/dist/claude-hooks-assets.d.ts +93 -0
  27. package/dist/claude-hooks-assets.d.ts.map +1 -0
  28. package/dist/claude-hooks-assets.js +208 -0
  29. package/dist/claude-hooks-assets.js.map +1 -0
  30. package/dist/clean-room-smoke.d.ts +83 -0
  31. package/dist/clean-room-smoke.d.ts.map +1 -0
  32. package/dist/clean-room-smoke.js +138 -0
  33. package/dist/clean-room-smoke.js.map +1 -0
  34. package/dist/cmd-usage.d.ts.map +1 -1
  35. package/dist/cmd-usage.js +36 -6
  36. package/dist/cmd-usage.js.map +1 -1
  37. package/dist/codex-hooks-assets.d.ts +25 -7
  38. package/dist/codex-hooks-assets.d.ts.map +1 -1
  39. package/dist/codex-hooks-assets.js +138 -30
  40. package/dist/codex-hooks-assets.js.map +1 -1
  41. package/dist/codex-hooks.d.ts +21 -1
  42. package/dist/codex-hooks.d.ts.map +1 -1
  43. package/dist/codex-hooks.js +21 -1
  44. package/dist/codex-hooks.js.map +1 -1
  45. package/dist/codex-invoke.d.ts +73 -0
  46. package/dist/codex-invoke.d.ts.map +1 -0
  47. package/dist/codex-invoke.js +80 -0
  48. package/dist/codex-invoke.js.map +1 -0
  49. package/dist/course-staleness.d.ts +19 -0
  50. package/dist/course-staleness.d.ts.map +1 -0
  51. package/dist/course-staleness.js +95 -0
  52. package/dist/course-staleness.js.map +1 -0
  53. package/dist/destructive-guard-hook.d.ts +40 -0
  54. package/dist/destructive-guard-hook.d.ts.map +1 -0
  55. package/dist/destructive-guard-hook.js +109 -0
  56. package/dist/destructive-guard-hook.js.map +1 -0
  57. package/dist/destructive-guard.d.ts +27 -0
  58. package/dist/destructive-guard.d.ts.map +1 -0
  59. package/dist/destructive-guard.js +2808 -0
  60. package/dist/destructive-guard.js.map +1 -0
  61. package/dist/discrimination-gate.d.ts +88 -3
  62. package/dist/discrimination-gate.d.ts.map +1 -1
  63. package/dist/discrimination-gate.js +173 -16
  64. package/dist/discrimination-gate.js.map +1 -1
  65. package/dist/event-chain.d.ts +30 -0
  66. package/dist/event-chain.d.ts.map +1 -1
  67. package/dist/event-chain.js +24 -0
  68. package/dist/event-chain.js.map +1 -1
  69. package/dist/feature-adr-routing.d.ts +95 -1
  70. package/dist/feature-adr-routing.d.ts.map +1 -1
  71. package/dist/feature-adr-routing.js +193 -27
  72. package/dist/feature-adr-routing.js.map +1 -1
  73. package/dist/guard.d.ts +98 -0
  74. package/dist/guard.d.ts.map +1 -1
  75. package/dist/guard.js +308 -0
  76. package/dist/guard.js.map +1 -1
  77. package/dist/harness-core-location.d.ts +18 -0
  78. package/dist/harness-core-location.d.ts.map +1 -0
  79. package/dist/harness-core-location.js +42 -0
  80. package/dist/harness-core-location.js.map +1 -0
  81. package/dist/index.d.ts +32 -11
  82. package/dist/index.d.ts.map +1 -1
  83. package/dist/index.js +32 -7
  84. package/dist/index.js.map +1 -1
  85. package/dist/lead-shift-gate.d.ts +95 -0
  86. package/dist/lead-shift-gate.d.ts.map +1 -0
  87. package/dist/lead-shift-gate.js +100 -0
  88. package/dist/lead-shift-gate.js.map +1 -0
  89. package/dist/ledger-backfill.d.ts +11 -1
  90. package/dist/ledger-backfill.d.ts.map +1 -1
  91. package/dist/ledger-backfill.js +19 -0
  92. package/dist/ledger-backfill.js.map +1 -1
  93. package/dist/loop-blobs.generated.d.ts +1 -1
  94. package/dist/loop-blobs.generated.d.ts.map +1 -1
  95. package/dist/loop-blobs.generated.js +12 -3
  96. package/dist/loop-blobs.generated.js.map +1 -1
  97. package/dist/loop-lint.d.ts.map +1 -1
  98. package/dist/loop-lint.js +56 -7
  99. package/dist/loop-lint.js.map +1 -1
  100. package/dist/loop-plan-graph.d.ts +1 -3
  101. package/dist/loop-plan-graph.d.ts.map +1 -1
  102. package/dist/loop-plan-graph.js +70 -1
  103. package/dist/loop-plan-graph.js.map +1 -1
  104. package/dist/loop-trace.d.ts.map +1 -1
  105. package/dist/loop-trace.js +16 -2
  106. package/dist/loop-trace.js.map +1 -1
  107. package/dist/managed-hooks.d.ts +5 -6
  108. package/dist/managed-hooks.d.ts.map +1 -1
  109. package/dist/managed-hooks.js +2 -2
  110. package/dist/managed-hooks.js.map +1 -1
  111. package/dist/model-recommender.d.ts.map +1 -1
  112. package/dist/model-recommender.js +14 -3
  113. package/dist/model-recommender.js.map +1 -1
  114. package/dist/mutation-gate.d.ts +39 -36
  115. package/dist/mutation-gate.d.ts.map +1 -1
  116. package/dist/mutation-gate.js +111 -5
  117. package/dist/mutation-gate.js.map +1 -1
  118. package/dist/operations.d.ts.map +1 -1
  119. package/dist/operations.js +90 -5
  120. package/dist/operations.js.map +1 -1
  121. package/dist/patterns.d.ts +23 -0
  122. package/dist/patterns.d.ts.map +1 -1
  123. package/dist/patterns.js +10 -0
  124. package/dist/patterns.js.map +1 -1
  125. package/dist/plugin.d.ts.map +1 -1
  126. package/dist/plugin.js +27 -5
  127. package/dist/plugin.js.map +1 -1
  128. package/dist/publish.d.ts +11 -0
  129. package/dist/publish.d.ts.map +1 -1
  130. package/dist/publish.js +16 -2
  131. package/dist/publish.js.map +1 -1
  132. package/dist/recommend.d.ts +4 -5
  133. package/dist/recommend.d.ts.map +1 -1
  134. package/dist/recommend.js +110 -45
  135. package/dist/recommend.js.map +1 -1
  136. package/dist/registry.d.ts +32 -1
  137. package/dist/registry.d.ts.map +1 -1
  138. package/dist/registry.js +168 -11
  139. package/dist/registry.js.map +1 -1
  140. package/dist/run-records.d.ts +3 -0
  141. package/dist/run-records.d.ts.map +1 -1
  142. package/dist/run-records.js +18 -0
  143. package/dist/run-records.js.map +1 -1
  144. package/dist/score.d.ts +95 -0
  145. package/dist/score.d.ts.map +1 -1
  146. package/dist/score.js +289 -6
  147. package/dist/score.js.map +1 -1
  148. package/dist/session-retro.d.ts +121 -2
  149. package/dist/session-retro.d.ts.map +1 -1
  150. package/dist/session-retro.js +454 -11
  151. package/dist/session-retro.js.map +1 -1
  152. package/dist/setup.d.ts +28 -0
  153. package/dist/setup.d.ts.map +1 -1
  154. package/dist/setup.js +223 -6
  155. package/dist/setup.js.map +1 -1
  156. package/dist/sign.d.ts.map +1 -1
  157. package/dist/sign.js +18 -1
  158. package/dist/sign.js.map +1 -1
  159. package/dist/skill-selection.d.ts +72 -0
  160. package/dist/skill-selection.d.ts.map +1 -0
  161. package/dist/skill-selection.js +76 -0
  162. package/dist/skill-selection.js.map +1 -0
  163. package/dist/skills-verify.d.ts +34 -1
  164. package/dist/skills-verify.d.ts.map +1 -1
  165. package/dist/skills-verify.js +82 -0
  166. package/dist/skills-verify.js.map +1 -1
  167. package/dist/stage-line.d.ts +68 -0
  168. package/dist/stage-line.d.ts.map +1 -0
  169. package/dist/stage-line.js +129 -0
  170. package/dist/stage-line.js.map +1 -0
  171. package/dist/statusline.d.ts +99 -0
  172. package/dist/statusline.d.ts.map +1 -1
  173. package/dist/statusline.js +310 -44
  174. package/dist/statusline.js.map +1 -1
  175. package/dist/stem.d.ts +12 -0
  176. package/dist/stem.d.ts.map +1 -0
  177. package/dist/stem.js +89 -0
  178. package/dist/stem.js.map +1 -0
  179. package/dist/store-counts.d.ts +26 -0
  180. package/dist/store-counts.d.ts.map +1 -0
  181. package/dist/store-counts.js +125 -0
  182. package/dist/store-counts.js.map +1 -0
  183. package/dist/store-guard.d.ts +106 -0
  184. package/dist/store-guard.d.ts.map +1 -0
  185. package/dist/store-guard.js +294 -0
  186. package/dist/store-guard.js.map +1 -0
  187. package/dist/swarm-brief.d.ts +95 -0
  188. package/dist/swarm-brief.d.ts.map +1 -0
  189. package/dist/swarm-brief.js +660 -0
  190. package/dist/swarm-brief.js.map +1 -0
  191. package/dist/telemetry-vocabulary.d.ts +7 -0
  192. package/dist/telemetry-vocabulary.d.ts.map +1 -1
  193. package/dist/telemetry-vocabulary.js +29 -0
  194. package/dist/telemetry-vocabulary.js.map +1 -1
  195. package/dist/trace-bundle.d.ts +8 -0
  196. package/dist/trace-bundle.d.ts.map +1 -1
  197. package/dist/trace-bundle.js +11 -0
  198. package/dist/trace-bundle.js.map +1 -1
  199. package/package.json +10 -9
  200. package/sbom.json +1033 -233
  201. package/src/agents-policy.ts +46 -2
  202. package/src/amendment-trace.ts +441 -19
  203. package/src/backlog-public.ts +503 -0
  204. package/src/backlog-transitions.ts +77 -0
  205. package/src/backlog.ts +12 -0
  206. package/src/claim-check.ts +25 -2
  207. package/src/claude-hooks-assets.ts +227 -0
  208. package/src/clean-room-smoke.ts +195 -0
  209. package/src/cmd-usage.ts +29 -5
  210. package/src/codex-hooks-assets.ts +140 -30
  211. package/src/codex-hooks.ts +21 -1
  212. package/src/codex-invoke.ts +138 -0
  213. package/src/course-staleness.ts +125 -0
  214. package/src/destructive-guard-hook.ts +151 -0
  215. package/src/destructive-guard.ts +3027 -0
  216. package/src/discrimination-gate.ts +262 -19
  217. package/src/event-chain.ts +41 -0
  218. package/src/feature-adr-routing.ts +220 -22
  219. package/src/guard.ts +358 -0
  220. package/src/harness-core-location.ts +44 -0
  221. package/src/index.ts +121 -7
  222. package/src/lead-shift-gate.ts +145 -0
  223. package/src/ledger-backfill.ts +20 -1
  224. package/src/loop-blobs.generated.ts +12 -3
  225. package/src/loop-lint.ts +52 -7
  226. package/src/loop-plan-graph.ts +66 -1
  227. package/src/loop-trace.ts +13 -1
  228. package/src/managed-hooks.ts +5 -6
  229. package/src/model-recommender.ts +14 -3
  230. package/src/mutation-gate.ts +165 -5
  231. package/src/operations.ts +83 -5
  232. package/src/patterns.ts +33 -0
  233. package/src/plugin.ts +27 -5
  234. package/src/publish.ts +27 -2
  235. package/src/recommend.ts +116 -46
  236. package/src/registry.ts +147 -13
  237. package/src/run-records.ts +23 -0
  238. package/src/score.ts +377 -7
  239. package/src/session-retro.ts +466 -11
  240. package/src/setup.ts +246 -9
  241. package/src/sign.ts +18 -1
  242. package/src/skill-selection.ts +111 -0
  243. package/src/skills-verify.ts +99 -1
  244. package/src/stage-line.ts +151 -0
  245. package/src/statusline.ts +396 -47
  246. package/src/stem.ts +87 -0
  247. package/src/store-counts.ts +154 -0
  248. package/src/store-guard.ts +388 -0
  249. package/src/swarm-brief.ts +661 -0
  250. package/src/telemetry-vocabulary.ts +36 -0
  251. package/src/trace-bundle.ts +10 -0
package/src/index.ts CHANGED
@@ -12,6 +12,39 @@ export const HARNESS_CORE_VERSION: string =
12
12
 
13
13
  export * from './skills.js';
14
14
  export * from './apply.js';
15
+ export {
16
+ buildPublicSnapshot,
17
+ assertPublicSafe,
18
+ applyDeltaRule,
19
+ HOLD_CAP_DAYS,
20
+ PUBLIC_SCHEMA,
21
+ MIN_GROUP,
22
+ TTT_BUCKETS,
23
+ type PrivateRecord,
24
+ type PublicSnapshot,
25
+ type BuildResult,
26
+ type RefusalCode,
27
+ type EgressVerdict,
28
+ type DeltaOutcome,
29
+ } from './backlog-public.js';
30
+
31
+ export {
32
+ appendTransition,
33
+ readTransitions,
34
+ transitionLogPath,
35
+ type TransitionRow,
36
+ } from './backlog-transitions.js';
37
+
38
+ export {
39
+ decideStop,
40
+ hasProgress,
41
+ BLOCK_BUDGET,
42
+ FRESH_HUMAN_MINUTES,
43
+ type LeadShift,
44
+ type ProgressEvidence,
45
+ type StopDecision,
46
+ } from './lead-shift-gate.js';
47
+
15
48
  export { bundleSkills } from './bundle.js';
16
49
  export type { BundleOptions, BundleResult, BundledSkill } from './bundle.js';
17
50
  export * from './targets.js';
@@ -75,6 +108,7 @@ export * from './trace-bundle.js';
75
108
  export { BLOBS as LOOP_BLOBS, LOOP_BLOB_NAMES, BLOB_COVERAGE_MANIFEST } from './loop-blobs.generated.js';
76
109
  export type { LoopBlob } from './loop-blobs.generated.js';
77
110
  export * from './sign.js';
111
+ export * from './swarm-brief.js';
78
112
  export * from './skill-schema.js';
79
113
  export { createSkill } from './create-skill.js';
80
114
  export type { CreateSkillOptions, CreateSkillResult } from './create-skill.js';
@@ -83,8 +117,8 @@ export type { SyncUpstreamReport, UpstreamCheckResult, SourcesManifest, SourcePa
83
117
  export { sweepSkillDrift, syncCanonicalSkill } from './skill-drift.js';
84
118
  export { SKILL_INSTALL_ROOTS, SKILL_INSTALL_ROOT_BY_TARGET, DEV_SKILL_ROOT, TARGET_ENRICHMENT_ASSETS } from './skill-install-roots.js';
85
119
  export { stampCheckpointLine } from './checkpoint-stamp.js';
86
- export { TELEMETRY_VOCAB_VERSION, TELEMETRY_FIELDS, PROVISIONAL_TELEMETRY_FIELDS, LOCAL_FIELD_ALIASES, telemetryFieldFor } from './telemetry-vocabulary.js';
87
- export type { TelemetryField, FieldSource } from './telemetry-vocabulary.js';
120
+ export { TELEMETRY_VOCAB_VERSION, TELEMETRY_FIELDS, PROVISIONAL_TELEMETRY_FIELDS, LOCAL_FIELD_ALIASES, RUN_OUTCOMES, telemetryFieldFor, runOutcomeOf } from './telemetry-vocabulary.js';
121
+ export type { TelemetryField, FieldSource, RunOutcome } from './telemetry-vocabulary.js';
88
122
  export { planLedgerBackfill, LEDGER_FILL_SOURCE, AMBIGUOUS, resolveLedgerRunId } from './ledger-backfill.js';
89
123
  // project-skills root resolution (field report doc-25b): the ONE builder behind both the Step-0
90
124
  // probe and the PS_GUIDANCE paragraph, so the two can never look at different roots again.
@@ -92,7 +126,8 @@ export { projectSkillsOneRoot, projectSkillsProbeCommand } from './project-skill
92
126
  export type { LedgerBackfillPlan, LedgerBackfillRow, RunCostFacts } from './ledger-backfill.js';
93
127
  export type { SweepResult, DriftedSkill, SyncResult, SyncCanonicalOptions } from './skill-drift.js';
94
128
  export { benchmarkSkill, benchmarkSkills, compareSkills } from './benchmark.js';
95
- export { buildRegistry, searchRegistry, filterByCategory, skillPackBaseDirs, discoverSkillPackDirs, discoverVerifiablePackDirs } from './registry.js';
129
+ export { buildRegistry, searchRegistry, filterByCategory, skillPackBaseDirs, discoverSkillPackDirs, discoverSkillCarryingDirs, discoverVerifiablePackDirs } from './registry.js';
130
+ export { tokenize, stemToken, stems } from './stem.js';
96
131
  // Package skill-layout resolution (feature dz-install-npx-init) — the ONE seam that knows where an
97
132
  // npm package keeps its skills (flat / templates/.claude/skills / skills). `cmdInstall` calls it;
98
133
  // `dz init`/`dz registry` are the filed follow-up consumers.
@@ -191,8 +226,32 @@ export type {
191
226
  } from './vector-tier.js';
192
227
  export { runSetup, generateHooksConfig, generateAgentdbWriter, writerVersionOf, AGENTDB_WRITER_VERSION,
193
228
  agentdbStorePath, agentdbMcpStorePath, agentdbStoreSeparationProblem } from './setup.js';
194
- export { statuslineData, readFeatureAdrState, writeFeatureAdrState, featureAdrStateDir, featureAdrStatePath } from './statusline.js';
195
- export type { StatuslineData, FeatureAdrState, WriteFeatureAdrStateInput } from './statusline.js';
229
+ export { countLearningStoreRowsReadonly } from './store-counts.js';
230
+ export type { LearningStoreRowCounts } from './store-counts.js';
231
+ export {
232
+ STORE_GUARD_VERSION,
233
+ STORE_COLLAPSE_MAX_FRACTION,
234
+ STORE_COLLAPSE_LAST_ROWS,
235
+ storeGuardPath,
236
+ storeSnapshotPath,
237
+ readStoreMark,
238
+ writeStoreMark,
239
+ resetStoreMark,
240
+ checkStoreHealth,
241
+ } from './store-guard.js';
242
+ export type {
243
+ StoreMark,
244
+ StoreHealth,
245
+ StoreHealthInput,
246
+ StoreHealthVerdict,
247
+ StoreCountSnapshot,
248
+ StoreCountSource,
249
+ StoreMarkObservation,
250
+ StoreMarkWriteOptions,
251
+ StoreResetReceipt,
252
+ } from './store-guard.js';
253
+ export { statuslineData, readFeatureAdrState, writeFeatureAdrState, featureAdrStateDir, featureAdrStatePath, writeFeatureAdrStateDetailed, renderFeatureAdrPhaseLine } from './statusline.js';
254
+ export type { StatuslineData, StatuslineStoreHealth, FeatureAdrState, WriteFeatureAdrStateInput, WriteFeatureAdrStateResult } from './statusline.js';
196
255
  export {
197
256
  ETA_MAX_STAGE_MS,
198
257
  estimateEta,
@@ -361,6 +420,10 @@ export {
361
420
  amendmentsMissingFromPlan,
362
421
  amendmentSubject,
363
422
  extractTestTitles,
423
+ amendmentIdsIn,
424
+ amendmentSectionCount,
425
+ amendmentDeclarationAmbiguity,
426
+ mentionsAmendmentId,
364
427
  } from './amendment-trace.js';
365
428
  export {
366
429
  RECORD_MAX_LINE_CHARS,
@@ -371,7 +434,7 @@ export {
371
434
  export { decidePublishSigning, decidePostSigningVerification, decideSignableSet, publishSigningLine, signableSetLine } from './publish-signing.js';
372
435
  export type { PublishSigningVerdict, PublishSigningDecision, SignableSetDecision } from './publish-signing.js';
373
436
  export type { RecordKind, RecordVerdict, RecordDecision } from './run-records.js';
374
- export type { AmendmentRow, AmendmentVerdict, AmendmentResolution, AmendmentOutcome, AmendmentDecision, PlanCoverageGap } from './amendment-trace.js';
437
+ export type { AmendmentAmbiguity, AmendmentRow, AmendmentVerdict, AmendmentResolution, AmendmentOutcome, AmendmentDecision, PlanCoverageGap } from './amendment-trace.js';
375
438
  // contract-checklist (ADR-001): pure extraction, canonical rendering, typed report parsing, and
376
439
  // exact per-item verification. Filesystem discovery/containment stays in harness-cli.
377
440
  export {
@@ -577,6 +640,7 @@ export {
577
640
  DEFAULT_REWRITE_ATTEMPTS,
578
641
  liveSegmentStart,
579
642
  classifyChainDefects,
643
+ CHAINED_JOURNALS,
580
644
  } from './event-chain.js';
581
645
  export type {
582
646
  ChainFields,
@@ -593,8 +657,10 @@ export type {
593
657
  VerifyEventChainOptions,
594
658
  ChainDefectAge,
595
659
  ChainDefectAges,
660
+ ChainedJournal,
596
661
  } from './event-chain.js';
597
662
  export { decideProvenance, environmentCanMintProvenance, publishArgv, discoverPackages, publishPackages, bumpPatch, compareVersions, findUnpackagedSkills, findUnpublishedWorkspaceFloors, orderByDependencies, syncReadmeVersion, isChangelogEntryLine, changelogRegion } from './publish.js';
663
+ export * from './course-staleness.js';
598
664
  export { fetchAllDownloads } from './downloads.js';
599
665
  export type { PackageDownloads, DownloadsReport } from './downloads.js';
600
666
  export { discoverInstalled, checkUpgrades } from './upgrade.js';
@@ -775,10 +841,17 @@ export {
775
841
  codexQeDeclineReason,
776
842
  codexDeclineReason,
777
843
  parseCodexReviewResult,
844
+ resolveStageDecision,
845
+ effectiveSpec,
846
+ specFamily,
847
+ qeReasonForFamilies,
848
+ STAGE_DECISION_REASONS,
778
849
  } from './feature-adr-routing.js';
779
850
  export type {
780
851
  StageOpts,
781
852
  RoutingEnv,
853
+ StageDecision,
854
+ StageDecisionReason,
782
855
  CrossFamilyQeReport,
783
856
  ModeBScopeVerdict,
784
857
  PartitionedFindings,
@@ -809,6 +882,15 @@ export type {
809
882
  CodexQeDeclineDetail,
810
883
  CodexReviewResult,
811
884
  } from './feature-adr-routing.js';
885
+ export {
886
+ STAGE_LINE_BLOB_VERSION,
887
+ STAGE_LINE_REASON_LABELS,
888
+ STAGE_LINE_BARRIER_STAGES,
889
+ renderStageIntentLine,
890
+ renderStageOutcomeLine,
891
+ renderStageLine,
892
+ } from './stage-line.js';
893
+ export type { StageLineDecision, StageLineOutcome, StageLineOutcomeState } from './stage-line.js';
812
894
  export {
813
895
  PARSER_SAFE_REGION_START,
814
896
  PARSER_SAFE_REGION_END,
@@ -961,7 +1043,8 @@ export {
961
1043
  } from './epoch-replay.js';
962
1044
 
963
1045
  // Run-process scorecard (feature dz-score, Reading C) — scores the DISCIPLINE of a feature-adr run
964
- // from its artifacts. Descriptive-only, permanently: it never gates.
1046
+ // from its artifacts and folds immutable receipts into a chained aggregate. Descriptive-only,
1047
+ // permanently: neither the single-run score nor the aggregate gates.
965
1048
  export * from './score.js';
966
1049
  export * from './recap.js';
967
1050
  export * from './provenance.js';
@@ -1037,3 +1120,34 @@ export type {
1037
1120
  Register, Domain, OperatorProfile, ProfileReadResult, ProfileSyncResult,
1038
1121
  ProfileDriftVerdict, ProfileDriftResult,
1039
1122
  } from './profile.js';
1123
+ export * from './codex-invoke.js';
1124
+ export * from './skill-selection.js';
1125
+
1126
+ // destructive-guard (feature destructive-command-guard, ADR-001) — pure pre-execution verdict on a
1127
+ // shell command. Refuses ONLY on a literal path into a protected store; `undecidable` is never
1128
+ // passed off as `refuse`.
1129
+ export { classifyDestructive, DESTRUCTIVE_RULES } from './destructive-guard.js';
1130
+ export type { DestructiveVerdict } from './destructive-guard.js';
1131
+
1132
+ // The CONSUMER half (task T8): one decision for both hosts — the Claude PreToolUse hook
1133
+ // (`.claude/hooks/destructive-guard.cjs`) and the emitted Codex veto helper call the SAME function,
1134
+ // so a refusal is worded identically on both runtimes.
1135
+ export {
1136
+ decideDestructiveHook,
1137
+ DESTRUCTIVE_REFUSE_MARKER,
1138
+ DESTRUCTIVE_WARN_MARKER,
1139
+ } from './destructive-guard-hook.js';
1140
+ export type { DestructiveHookDecision, DestructiveHookHost } from './destructive-guard-hook.js';
1141
+
1142
+ // Delivery of the guard into OTHER projects (task T8, review round 3): the installable Claude hook
1143
+ // body + its registry entry, and the locator that lets an emitted body find the installation that
1144
+ // wrote it. Both hosts' bodies resolve harness-core by PATH, never by bare specifier.
1145
+ export {
1146
+ generateClaudeDestructiveHook,
1147
+ CLAUDE_DESTRUCTIVE_HOOK_COMMAND,
1148
+ CLAUDE_DESTRUCTIVE_HOOK_MATCHER,
1149
+ CLAUDE_DESTRUCTIVE_HOOK_RELPATH,
1150
+ DZ_CLAUDE_HOOK_VERSION,
1151
+ } from './claude-hooks-assets.js';
1152
+ export type { ClaudeHookAssetOptions } from './claude-hooks-assets.js';
1153
+ export { harnessCoreDistDir, harnessCoreModuleDir } from './harness-core-location.js';
@@ -0,0 +1,145 @@
1
+ /**
2
+ * Сторож смены лида: отказать в завершении хода, пока санкционированная работа не кончилась.
3
+ *
4
+ * ЗАЧЕМ ЭТО СУЩЕСТВУЕТ — два измеренных происшествия, второе после починки первого.
5
+ *
6
+ * 2026-08-29: простой ~4,5 часа. Лид «мониторил» задачи, чьи родители давно умерли, и считал
7
+ * очередь непустой. Лечением стало ПРАВИЛО в `.claude/rules/lead-idle-discipline.md`.
8
+ *
9
+ * 2026-09-03: простой **6 часов 24 минуты**. Лид отработал 19:16–23:15, написал утренний отчёт,
10
+ * ЗАВЕРШИЛ ХОД и молчал до 05:38. Очередь была не просто непустой — 352 открытые записи, и
11
+ * незакрытая работа была перечислена самим лидом абзацем выше в том же отчёте. Правило,
12
+ * написанное после первого случая, читалось каждый прогон и не сработало.
13
+ *
14
+ * Вывод, который и породил этот файл: **текст второго слоя лестницы стоимости обнаружения не
15
+ * ловит собственный молчаливый отказ.** Правило нельзя «не выполнить с ошибкой» — его можно
16
+ * только не вспомнить, и снаружи это неотличимо от выполнения. Поэтому решение переносится на
17
+ * слой 1: детерминированная функция, у которой есть красный тест.
18
+ *
19
+ * СТРУКТУРНАЯ ТРУДНОСТЬ, названная честно: завершение хода — это ОТСУТСТВИЕ дальнейших вызовов,
20
+ * а не событие. Перехватить «модель перестала работать» нельзя. Зато харнесс даёт шов `Stop`,
21
+ * который срабатывает на попытке закончить ход и умеет её ЗАБЛОКИРОВАТЬ. Этот файл — решающая
22
+ * половина такого гейта; вторая половина, тонкий адаптер, живёт в хуке.
23
+ *
24
+ * АТАКА, СЛОМАВШАЯ ПЕРВУЮ РЕДАКЦИЮ (Codex gpt-5.6-sol, xhigh, 2026-09-03, вердикт «НЕ СРАБОТАЕТ»).
25
+ * Первая редакция разрешала ПОВТОРНУЮ остановку автоматически — как защиту от зацикливания.
26
+ * Приём обхода: получить отказ, ответить «принято, продолжу», не сделать ни одного рабочего
27
+ * действия, остановиться снова — и второй раз пропускали. Итог был бы тот же простой плюс одна
28
+ * запись в журнале. Поэтому здесь повторная остановка разрешается НЕ ПО ФАКТУ ПОВТОРНОСТИ, а
29
+ * только по машинному свидетельству прогресса (см. `hasProgress`).
30
+ */
31
+
32
+ /** Машинное свидетельство того, что между двумя попытками остановиться что-то произошло. */
33
+ export interface ProgressEvidence {
34
+ /** `git rev-parse HEAD` на момент прошлой блокировки и сейчас. */
35
+ readonly headBefore?: string | undefined;
36
+ readonly headNow?: string | undefined;
37
+ /** sha256 от `git status --porcelain` — ловит правку, ещё не ставшую коммитом. */
38
+ readonly treeHashBefore?: string | undefined;
39
+ readonly treeHashNow?: string | undefined;
40
+ /** Идентификаторы живых фоновых задач тогда и сейчас. */
41
+ readonly liveTasksBefore?: readonly string[] | undefined;
42
+ readonly liveTasksNow?: readonly string[] | undefined;
43
+ /**
44
+ * Пункты очереди, закрытые между попытками, вместе с доказательством закрытия.
45
+ * `evidenceExists` считает ВЫЗЫВАЮЩИЙ, проверив путь или коммит на существование: закрытие
46
+ * без существующего доказательства прогрессом не считается, иначе «закрыть» станет строкой.
47
+ */
48
+ readonly closedItems?: readonly { readonly id: string; readonly evidenceExists: boolean }[] | undefined;
49
+ }
50
+
51
+ /** Состояние смены: что владелец санкционировал и в каком режиме идёт работа. */
52
+ export interface LeadShift {
53
+ /** `autonomous` — владелец отпустил лида работать без присмотра; иначе сторож спит. */
54
+ readonly mode: 'autonomous' | 'interactive';
55
+ /** Открытых пунктов в санкционированной очереди. */
56
+ readonly openItems: number;
57
+ /** Живая работа: фоновые задачи, чьи родители ЖИВЫ. Пустой список — не повод спать. */
58
+ readonly liveWork: readonly string[];
59
+ /** Минут с последнего сообщения ЧЕЛОВЕКА. Свежий разговор — законная причина остановиться. */
60
+ readonly minutesSinceHuman: number;
61
+ /** Это повторная попытка остановиться после нашей же блокировки. */
62
+ readonly stopHookActive: boolean;
63
+ /** Сколько раз подряд мы уже блокировали в этой смене. */
64
+ readonly consecutiveBlocks: number;
65
+ /** Квитанция завершения из закрытого списка: владелец сказал стоп, очередь пуста, работа блокирована. */
66
+ readonly completionReceipt?: 'owner-stop' | 'queue-empty' | 'blocked-on-owner' | undefined;
67
+ }
68
+
69
+ /** Что сторож решил и почему. `reason` пишется в журнал инцидентов и показывается лиду. */
70
+ export interface StopDecision {
71
+ readonly action: 'allow' | 'block' | 'escalate';
72
+ readonly reason: string;
73
+ /** Машинный код причины — для журнала и для тестов; текст `reason` меняется, код нет. */
74
+ readonly code:
75
+ | 'not-autonomous' | 'receipt' | 'live-work' | 'fresh-human' | 'queue-empty'
76
+ | 'idle-with-queue' | 'no-progress' | 'budget-exhausted';
77
+ }
78
+
79
+ /** Потолок блокировок подряд. Ниже харнессового потолка в 8, чтобы решение осталось нашим. */
80
+ export const BLOCK_BUDGET = 3;
81
+
82
+ /** Свежий разговор с человеком: остановиться сразу после его реплики — законно. */
83
+ export const FRESH_HUMAN_MINUTES = 20;
84
+
85
+ /**
86
+ * Было ли между двумя попытками остановиться хоть одно машинно заметное движение.
87
+ *
88
+ * Ответ «принято, продолжу работу» не меняет ни коммита, ни дерева, ни списка задач, ни очереди —
89
+ * и потому прогрессом не является. В этом весь смысл функции: она отличает СЛОВА от ДЕЙСТВИЙ.
90
+ */
91
+ export function hasProgress(e: ProgressEvidence | undefined): boolean {
92
+ if (!e) return false;
93
+ if (e.headBefore && e.headNow && e.headBefore !== e.headNow) return true;
94
+ if (e.treeHashBefore && e.treeHashNow && e.treeHashBefore !== e.treeHashNow) return true;
95
+ const before = new Set(e.liveTasksBefore ?? []);
96
+ if ((e.liveTasksNow ?? []).some((id) => !before.has(id))) return true;
97
+ // Закрытие пункта считается прогрессом ТОЛЬКО с существующим доказательством: иначе достаточно
98
+ // объявить пункт закрытым, чтобы получить право замолчать.
99
+ if ((e.closedItems ?? []).some((c) => c.evidenceExists)) return true;
100
+ return false;
101
+ }
102
+
103
+ /**
104
+ * Решить, можно ли закончить ход.
105
+ *
106
+ * Порядок проверок load-bearing: сначала всё, что делает остановку ЗАКОННОЙ, и только потом
107
+ * запрет. Сторож, который сначала запрещает, будет отключён после первого же ложного срабатывания
108
+ * на правильной остановке — а отключённый сторож не ловит ничего.
109
+ */
110
+ export function decideStop(shift: LeadShift, progress?: ProgressEvidence): StopDecision {
111
+ if (shift.mode !== 'autonomous') {
112
+ return { action: 'allow', code: 'not-autonomous', reason: 'смена не в автономном режиме — остановка не наше дело' };
113
+ }
114
+ if (shift.completionReceipt) {
115
+ return { action: 'allow', code: 'receipt', reason: `квитанция завершения: ${shift.completionReceipt}` };
116
+ }
117
+ if (shift.liveWork.length > 0) {
118
+ return { action: 'allow', code: 'live-work', reason: `живая работа: ${shift.liveWork.join(', ')}` };
119
+ }
120
+ if (shift.minutesSinceHuman < FRESH_HUMAN_MINUTES) {
121
+ return { action: 'allow', code: 'fresh-human', reason: `человек писал ${shift.minutesSinceHuman} мин назад — разговор идёт` };
122
+ }
123
+ if (shift.openItems === 0) {
124
+ return { action: 'allow', code: 'queue-empty', reason: 'очередь пуста' };
125
+ }
126
+ // Дальше — остановка при непустой очереди, без живой работы и без квитанции. Ровно этот случай
127
+ // произошёл 2026-09-03 и стоил 6 часов 24 минут.
128
+ if (shift.consecutiveBlocks >= BLOCK_BUDGET) {
129
+ return {
130
+ action: 'escalate',
131
+ code: 'budget-exhausted',
132
+ reason: `${shift.consecutiveBlocks} блокировки подряд без прогресса — будим владельца и разрешаем остановку`,
133
+ };
134
+ }
135
+ if (shift.stopHookActive && hasProgress(progress)) {
136
+ return { action: 'allow', code: 'no-progress', reason: 'после прошлой блокировки есть машинный прогресс' };
137
+ }
138
+ return {
139
+ action: 'block',
140
+ code: shift.stopHookActive ? 'no-progress' : 'idle-with-queue',
141
+ reason: shift.stopHookActive
142
+ ? `повторная остановка без единого машинного признака прогресса; открытых пунктов ${shift.openItems}`
143
+ : `в очереди ${shift.openItems} открытых пунктов, живой работы нет — ход не заканчивается`,
144
+ };
145
+ }
@@ -59,7 +59,7 @@ export interface LedgerBackfillRow {
59
59
  readonly key: LedgerFillKey | null;
60
60
  readonly filled: readonly string[];
61
61
  /** Why nothing was filled. `null` when something was. */
62
- readonly skipped: 'no-join-key' | 'no-host-record' | 'ambiguous-slug' | 'shared-run-claim' | 'already-complete' | 'malformed-line' | 'host-record-empty' | null;
62
+ readonly skipped: 'no-join-key' | 'no-host-record' | 'ambiguous-slug' | 'shared-run-claim' | 'already-complete' | 'malformed-line' | 'host-record-empty' | 'non-run-row' | null;
63
63
  }
64
64
 
65
65
  export interface LedgerBackfillPlan {
@@ -75,6 +75,17 @@ const FILLABLE = ['tokens', 'minutes', 'agents'] as const;
75
75
 
76
76
  const isNum = (v: unknown): v is number => typeof v === 'number' && Number.isFinite(v);
77
77
 
78
+ /**
79
+ * fa-phase-statusline QE fix (P2, cross-family review of 656d6903): the ledger now carries
80
+ * per-PHASE telemetry rows (`kind:"phase"`, schema `feature-adr-phase/1`) alongside its run rows.
81
+ * The header's contract: the one-line-one-run invariant applies ONLY to rows WITHOUT a `kind`
82
+ * field, so any row that carries one is NOT a run row and must be invisible to the backfill —
83
+ * both to claimant counting (several phase rows under a slug were making a real cost row
84
+ * `shared-run-claim`, unfillable forever) and to filling (exactly one phase row under a slug could
85
+ * receive the whole run's token total — a fabrication by attribution).
86
+ */
87
+ export const isNonRunRow = (r: Record<string, unknown>): boolean => typeof r['kind'] === 'string' && r['kind'] !== '';
88
+
78
89
  /**
79
90
  * Plan the fill. PURE: takes the ledger's raw lines and a runId→facts lookup, returns the new lines.
80
91
  * Nothing is read or written here — the caller owns the file and the atomic replace.
@@ -100,6 +111,7 @@ export function planLedgerBackfill(input: {
100
111
  const parsed: unknown = JSON.parse(raw);
101
112
  if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) continue;
102
113
  const r = parsed as Record<string, unknown>;
114
+ if (isNonRunRow(r)) continue; // phase rows are telemetry, never claimants
103
115
  if (FILLABLE.every((f) => isNum(r[f]))) continue;
104
116
  const rid = typeof r['runId'] === 'string' && r['runId'] !== '' ? r['runId'] : null;
105
117
  const slg = typeof r['slug'] === 'string' && r['slug'] !== '' ? r['slug'] : null;
@@ -129,6 +141,13 @@ export function planLedgerBackfill(input: {
129
141
 
130
142
  const runId = typeof row['runId'] === 'string' && row['runId'] !== '' ? row['runId'] : null;
131
143
  const slug = typeof row['slug'] === 'string' && row['slug'] !== '' ? row['slug'] : null;
144
+ if (isNonRunRow(row)) {
145
+ // A `kind`-carrying row (per-phase telemetry) has no run cost to fill — pass through
146
+ // unchanged and SAY so, never let it receive a run's numbers.
147
+ outLines.push(raw);
148
+ rows.push({ index, runId, slug, key: null, filled: [], skipped: 'non-run-row' });
149
+ return;
150
+ }
132
151
  const missing = FILLABLE.filter((f) => !isNum(row[f]));
133
152
  if (missing.length === 0) {
134
153
  outLines.push(raw);
@@ -25,7 +25,7 @@ export interface LoopBlob {
25
25
  code: string;
26
26
  }
27
27
 
28
- export const LOOP_BLOB_NAMES = ["checkpoints","training-pairs","model-resolver","usage-probes","codex-dispatch","challenge-panel","trace","loop-semantics","ha-consult-router"] as const;
28
+ export const LOOP_BLOB_NAMES = ["checkpoints","training-pairs","model-resolver","usage-probes","codex-dispatch","challenge-panel","stage-line","trace","loop-semantics","ha-consult-router"] as const;
29
29
 
30
30
  /** Workflow files the regen-diff gate covers TODAY (AM-5 honest scope): exactly the files
31
31
  * carrying BEGIN BLOB markers. Stage B (whole-file regeneration of feature-adr.js) is a
@@ -61,11 +61,11 @@ export const BLOBS: Record<string, LoopBlob> = {
61
61
  "model-resolver": {
62
62
  name: "model-resolver",
63
63
  version: "1.0.0",
64
- contentHash: "fcf8c1dfc4b5364adb6e532565953ef30d94a45070b2b903411170965a1fb372",
64
+ contentHash: "357a258796e553b2fbe94dfab26cf487a9fd189b4927836f261185215392dfaa",
65
65
  sourcePath: "packages/@dzhechkov/harness-core/src/feature-adr-routing.ts",
66
66
  requires: [],
67
67
  exports: ["specToOpts","resolveStageModel","KNOWN_CODEX","mergeOpts","stageLabel","modelLabel"],
68
- code: "const STAGE_EFFORT = { override: {\n router: 'medium',\n requirements: 'medium',\n research: 'medium',\n adr: 'high',\n ideation: 'medium',\n ddd: 'high',\n architecture: 'high',\n plan: 'high',\n code: 'medium',\n qe: 'high',\n fleet: 'medium',\n } };\nfunction topCodexId(env) {\n return env.CODEX_MODEL !== 'auto' ? env.CODEX_MODEL : CODEX_TIERS.flagship;\n}\nconst KNOWN_CODEX = { auto: 1, 'gpt-5.5': 1, 'gpt-5.6': 1, 'gpt-5.6-luna': 1, 'gpt-5.6-terra': 1, 'gpt-5.6-sol': 1 };\nconst CODEX_TIERS = {\n flagship: 'gpt-5.6-sol',\n workhorse: 'gpt-5.6-terra',\n 'high-volume': 'gpt-5.6-luna',\n};\nfunction codexIdForTier(tier, env) {\n return env.CODEX_MODEL !== 'auto' ? env.CODEX_MODEL : CODEX_TIERS[tier];\n}\nconst BUDGET_PRESETS = {\n normal: { claude: 'normal', codex: 'normal' },\n eco: { claude: 'eco', codex: 'eco' },\n hybrid: { claude: 'eco', codex: 'normal' },\n};\nfunction resolveBudgetMode(raw) {\n if (raw === undefined)\n return BUDGET_PRESETS.normal;\n if (typeof raw === 'string') {\n const preset = BUDGET_PRESETS[raw];\n if (!preset)\n throw new RangeError('budget: unknown preset \"' + raw + '\" — valid: normal|eco|hybrid');\n return preset;\n }\n if (raw && typeof raw === 'object') {\n const value = raw;\n for (const key of Object.keys(value)) {\n if (key !== 'claude' && key !== 'codex') {\n throw new RangeError('budget: unknown family key \"' + key + '\" — valid: claude|codex');\n }\n }\n for (const key of ['claude', 'codex']) {\n const level = value[key];\n if (level !== undefined && level !== 'normal' && level !== 'eco') {\n throw new RangeError('budget.' + key + ': unknown level \"' + level + '\" — valid: normal|eco');\n }\n }\n return {\n claude: value.claude || 'normal',\n codex: value.codex || 'normal',\n };\n }\n throw new RangeError('budget: expected a preset name or {claude,codex} object, got ' + typeof raw);\n}\nconst CLAUDE_NAMES = { fable: 1, opus: 1, sonnet: 1, haiku: 1 };\nconst VALID_REASONING = { none: 1, minimal: 1, low: 1, medium: 1, high: 1, xhigh: 1, max: 1 };\nconst DEFAULT_MODELS = {\n router: 'fable',\n requirements: 'sonnet',\n research: 'sonnet',\n adr: 'opus',\n ideation: 'sonnet',\n ddd: 'opus',\n architecture: 'opus',\n plan: 'sonnet',\n code: null,\n qe: null,\n fleet: 'sonnet',\n};\nconst ROUTING_TABLES = {\n claude: {\n claude: {\n normal: { router: 'sonnet', requirements: 'sonnet', research: 'sonnet', adr: 'fable', ideation: 'sonnet', ddd: 'fable', architecture: 'fable', plan: 'opus', code: 'sonnet', fleet: 'sonnet' },\n eco: { router: 'sonnet', requirements: 'sonnet', research: 'sonnet', adr: 'opus', ideation: 'sonnet', ddd: 'opus', architecture: 'opus', plan: 'sonnet', code: 'sonnet', fleet: 'sonnet' },\n },\n codex: { normal: {}, eco: {} },\n },\n codex: {\n claude: {\n normal: { router: 'sonnet', qe: 'sonnet', fleet: 'sonnet' },\n eco: { router: 'sonnet', qe: 'sonnet', fleet: 'sonnet' },\n },\n codex: { normal: {}, eco: {} },\n },\n};\nfunction codexCell(tier, effort, env) {\n return 'codex:' + codexIdForTier(tier, env) + ':' + effort;\n}\nfunction budgetTable(primary, mode, env) {\n const claudeHalf = ROUTING_TABLES[primary].claude[mode.claude];\n let codexHalf;\n if (primary === 'claude') {\n const qeSpec = mode.codex === 'normal'\n ? codexCell('flagship', 'high', env)\n : codexCell('workhorse', 'medium', env);\n codexHalf = { ...ROUTING_TABLES.claude.codex[mode.codex], qe: env.codexAvailable === false ? 'opus' : qeSpec };\n }\n else {\n const normal = mode.codex === 'normal';\n const design = codexCell(normal ? 'flagship' : 'workhorse', normal ? 'high' : 'medium', env);\n codexHalf = {\n requirements: design,\n research: design,\n adr: design,\n ideation: design,\n ddd: design,\n architecture: design,\n plan: codexCell(normal ? 'flagship' : 'workhorse', normal ? 'high' : 'low', env),\n code: codexCell(normal ? 'flagship' : 'workhorse', 'medium', env),\n };\n }\n return { ...claudeHalf, ...codexHalf };\n}\nfunction specToOpts(spec, env) {\n const log = env.log || function () { };\n if (!spec)\n return {};\n const parts = String(spec).split(':');\n const head = parts[0] || '';\n if (head === 'codex') {\n let id = parts[1] || env.CODEX_MODEL;\n if (id !== 'auto' && !KNOWN_CODEX[id]) {\n log('models: unknown codex id ' + id + ' — using ' + env.CODEX_MODEL);\n id = env.CODEX_MODEL;\n }\n let reasoning = parts[2] || 'high';\n if (!VALID_REASONING[reasoning]) {\n throw new RangeError('models: invalid reasoning \"' + reasoning + '\" — valid: ' + Object.keys(VALID_REASONING).join('|'));\n }\n return { agentType: 'codex:codex-rescue', codexModel: id, _reasoning: reasoning };\n }\n if (CLAUDE_NAMES[head])\n return { model: head };\n log('models: unknown spec ' + spec + ' — session-inherited');\n return {};\n}\nfunction resolveCoderSpec(env) {\n if (env.CODER === 'codex' || env.CODER === 'codex-fallback')\n return 'codex:' + env.CODEX_MODEL + ':high';\n return 'opus';\n}\nfunction coderIsCodex(env) {\n const codeSpec = env.MODELS.code;\n if (codeSpec !== undefined && codeSpec !== null)\n return String(codeSpec).split(':')[0] === 'codex';\n if (env.CODER === 'codex' || env.CODER === 'codex-fallback')\n return true;\n return env.primary === 'codex';\n}\nfunction resolveQeSpecForCoder(coderCodex, env) {\n if (coderCodex)\n return 'sonnet';\n const CODEX_AVAILABLE = env.codexAvailable !== false;\n if (!CODEX_AVAILABLE)\n return 'opus';\n const budget = resolveBudgetMode(env.budget);\n return budget.codex === 'eco'\n ? 'codex:' + codexIdForTier('workhorse', env) + ':medium'\n : 'codex:' + codexIdForTier('flagship', env) + ':high';\n}\nfunction resolveQeSpec(env) {\n return resolveQeSpecForCoder(coderIsCodex(env), env);\n}\nfunction routingRequested(env) {\n return (Object.keys(env.MODELS).length > 0 ||\n env.primary !== undefined ||\n env.budget !== undefined ||\n env.PLANNER === 'codex' ||\n env.CODER === 'codex' ||\n env.CODER === 'codex-fallback' ||\n env.QE_REVIEWER === 'codex' ||\n env.QE_REVIEWER === 'codex-fallback');\n}\nfunction resolveStageModel(stage, env) {\n if (env.usageOverride) {\n const r = (env.usageReasoning && env.usageReasoning[stage]) || STAGE_EFFORT.override[stage] || 'medium';\n const o = specToOpts('codex:' + topCodexId(env) + ':' + r, env);\n o._usageSwitched = true;\n return o;\n }\n let spec = env.MODELS[stage];\n if (spec === undefined) {\n if (!routingRequested(env))\n return {};\n if (stage === 'code' && (env.CODER === 'codex' || env.CODER === 'codex-fallback')) {\n return specToOpts(resolveCoderSpec(env), env);\n }\n if (stage === 'plan' && env.PLANNER === 'codex') {\n return specToOpts('codex:' + env.CODEX_MODEL + ':high', env);\n }\n if (stage === 'qe') {\n return specToOpts(resolveQeSpec(env), env);\n }\n const resolvedPrimary = env.primary || 'claude';\n const cell = budgetTable(resolvedPrimary, resolveBudgetMode(env.budget), env)[stage];\n spec = cell !== undefined ? cell : DEFAULT_MODELS[stage];\n }\n if (stage === 'code' && (spec === null || spec === undefined))\n return specToOpts(resolveCoderSpec(env), env);\n if (stage === 'qe' && (spec === null || spec === undefined))\n return specToOpts(resolveQeSpec(env), env);\n return specToOpts(spec, env);\n}\nfunction modelLabel(opts) {\n if (opts && opts.agentType === 'codex:codex-rescue') {\n const base = 'codex:' + opts.codexModel + ':' + opts._reasoning;\n return opts._usageSwitched ? base + ' (usage-switched)' : base;\n }\n if (opts && opts.model)\n return opts.model;\n return 'session';\n}\nfunction stageLabel(base, opts) {\n const m = modelLabel(opts);\n return m === 'session' ? base : base + ' · ' + m;\n}\nfunction mergeOpts(base, extra) {\n const out = {};\n for (const k in base)\n out[k] = base[k];\n for (const k in extra)\n out[k] = extra[k];\n return out;\n}",
68
+ code: "const STAGE_EFFORT = { override: {\n router: 'medium',\n requirements: 'medium',\n research: 'medium',\n adr: 'high',\n ideation: 'medium',\n ddd: 'high',\n architecture: 'high',\n plan: 'high',\n code: 'medium',\n qe: 'high',\n fleet: 'medium',\n } };\nfunction topCodexId(env) {\n return env.CODEX_MODEL !== 'auto' ? env.CODEX_MODEL : CODEX_TIERS.flagship;\n}\nconst KNOWN_CODEX = { auto: 1, 'gpt-5.5': 1, 'gpt-5.6': 1, 'gpt-5.6-luna': 1, 'gpt-5.6-terra': 1, 'gpt-5.6-sol': 1 };\nconst CODEX_TIERS = {\n flagship: 'gpt-5.6-sol',\n workhorse: 'gpt-5.6-terra',\n 'high-volume': 'gpt-5.6-luna',\n};\nfunction codexIdForTier(tier, env) {\n return env.CODEX_MODEL !== 'auto' ? env.CODEX_MODEL : CODEX_TIERS[tier];\n}\nconst BUDGET_PRESETS = {\n normal: { claude: 'normal', codex: 'normal' },\n eco: { claude: 'eco', codex: 'eco' },\n hybrid: { claude: 'eco', codex: 'normal' },\n};\nfunction resolveBudgetMode(raw) {\n if (raw === undefined)\n return BUDGET_PRESETS.normal;\n if (typeof raw === 'string') {\n const preset = BUDGET_PRESETS[raw];\n if (!preset)\n throw new RangeError('budget: unknown preset \"' + raw + '\" — valid: normal|eco|hybrid');\n return preset;\n }\n if (raw && typeof raw === 'object') {\n const value = raw;\n for (const key of Object.keys(value)) {\n if (key !== 'claude' && key !== 'codex') {\n throw new RangeError('budget: unknown family key \"' + key + '\" — valid: claude|codex');\n }\n }\n for (const key of ['claude', 'codex']) {\n const level = value[key];\n if (level !== undefined && level !== 'normal' && level !== 'eco') {\n throw new RangeError('budget.' + key + ': unknown level \"' + level + '\" — valid: normal|eco');\n }\n }\n return {\n claude: value.claude || 'normal',\n codex: value.codex || 'normal',\n };\n }\n throw new RangeError('budget: expected a preset name or {claude,codex} object, got ' + typeof raw);\n}\nconst CLAUDE_NAMES = { fable: 1, opus: 1, sonnet: 1, haiku: 1 };\nconst VALID_REASONING = { none: 1, minimal: 1, low: 1, medium: 1, high: 1, xhigh: 1, max: 1 };\nconst DEFAULT_MODELS = {\n router: 'fable',\n requirements: 'sonnet',\n research: 'sonnet',\n adr: 'opus',\n ideation: 'sonnet',\n ddd: 'opus',\n architecture: 'opus',\n plan: 'sonnet',\n code: null,\n qe: null,\n fleet: 'sonnet',\n};\nconst ROUTING_TABLES = {\n claude: {\n claude: {\n normal: { router: 'sonnet', requirements: 'sonnet', research: 'sonnet', adr: 'fable', ideation: 'sonnet', ddd: 'fable', architecture: 'fable', plan: 'opus', code: 'sonnet', fleet: 'sonnet' },\n eco: { router: 'sonnet', requirements: 'sonnet', research: 'sonnet', adr: 'opus', ideation: 'sonnet', ddd: 'opus', architecture: 'opus', plan: 'sonnet', code: 'sonnet', fleet: 'sonnet' },\n },\n codex: { normal: {}, eco: {} },\n },\n codex: {\n claude: {\n normal: { router: 'sonnet', qe: 'sonnet', fleet: 'sonnet' },\n eco: { router: 'sonnet', qe: 'sonnet', fleet: 'sonnet' },\n },\n codex: { normal: {}, eco: {} },\n },\n};\nfunction codexCell(tier, effort, env) {\n return 'codex:' + codexIdForTier(tier, env) + ':' + effort;\n}\nfunction budgetTable(primary, mode, env) {\n const claudeHalf = ROUTING_TABLES[primary].claude[mode.claude];\n let codexHalf;\n if (primary === 'claude') {\n const qeSpec = mode.codex === 'normal'\n ? codexCell('flagship', 'high', env)\n : codexCell('workhorse', 'medium', env);\n codexHalf = { ...ROUTING_TABLES.claude.codex[mode.codex], qe: env.codexAvailable === false ? 'opus' : qeSpec };\n }\n else {\n const normal = mode.codex === 'normal';\n const design = codexCell(normal ? 'flagship' : 'workhorse', normal ? 'high' : 'medium', env);\n codexHalf = {\n requirements: design,\n research: design,\n adr: design,\n ideation: design,\n ddd: design,\n architecture: design,\n plan: codexCell(normal ? 'flagship' : 'workhorse', normal ? 'high' : 'low', env),\n code: codexCell(normal ? 'flagship' : 'workhorse', 'medium', env),\n };\n }\n return { ...claudeHalf, ...codexHalf };\n}\nfunction specToOpts(spec, env) {\n const log = env.log || function () { };\n if (!spec)\n return {};\n const parts = String(spec).split(':');\n const head = parts[0] || '';\n if (head === 'codex') {\n let id = parts[1] || env.CODEX_MODEL;\n if (id !== 'auto' && !KNOWN_CODEX[id]) {\n log('models: unknown codex id ' + id + ' — using ' + env.CODEX_MODEL);\n id = env.CODEX_MODEL;\n }\n let reasoning = parts[2] || 'high';\n if (!VALID_REASONING[reasoning]) {\n throw new RangeError('models: invalid reasoning \"' + reasoning + '\" — valid: ' + Object.keys(VALID_REASONING).join('|'));\n }\n return { agentType: 'codex:codex-rescue', codexModel: id, _reasoning: reasoning };\n }\n if (CLAUDE_NAMES[head])\n return { model: head };\n log('models: unknown spec ' + spec + ' — session-inherited');\n return {};\n}\nfunction resolveCoderSpec(env) {\n if (env.CODER === 'codex' || env.CODER === 'codex-fallback')\n return 'codex:' + env.CODEX_MODEL + ':high';\n return 'opus';\n}\nfunction coderIsCodex(env) {\n const codeSpec = env.MODELS.code;\n if (codeSpec !== undefined && codeSpec !== null)\n return String(codeSpec).split(':')[0] === 'codex';\n if (env.CODER === 'codex' || env.CODER === 'codex-fallback')\n return true;\n return env.primary === 'codex';\n}\nfunction resolveQeSpecForCoder(coderCodex, env) {\n if (coderCodex)\n return 'sonnet';\n const CODEX_AVAILABLE = env.codexAvailable !== false;\n if (!CODEX_AVAILABLE)\n return 'opus';\n const budget = resolveBudgetMode(env.budget);\n return budget.codex === 'eco'\n ? 'codex:' + codexIdForTier('workhorse', env) + ':medium'\n : 'codex:' + codexIdForTier('flagship', env) + ':high';\n}\nfunction resolveQeSpec(env) {\n return resolveQeSpecForCoder(coderIsCodex(env), env);\n}\nfunction routingRequested(env) {\n return (Object.keys(env.MODELS).length > 0 ||\n env.primary !== undefined ||\n env.budget !== undefined ||\n env.PLANNER === 'codex' ||\n env.CODER === 'codex' ||\n env.CODER === 'codex-fallback' ||\n env.QE_REVIEWER === 'codex' ||\n env.QE_REVIEWER === 'codex-fallback');\n}\nfunction resolveStageModel(stage, env) {\n return resolveStageDecision(stage, env).opts;\n}\nfunction specFamily(spec) {\n return (spec && String(spec).split(':')[0] === 'codex') ? 'codex' : 'claude';\n}\nfunction qeReasonForFamilies(coderCodex, qeSpec) {\n if (specFamily(qeSpec) === (coderCodex ? 'codex' : 'claude'))\n return 'qe-same-family-degraded';\n return 'qe-cross-family';\n}\nfunction effectiveSpec(opts) {\n if (opts && opts.agentType === 'codex:codex-rescue')\n return 'codex:' + opts.codexModel + ':' + opts._reasoning;\n if (opts && opts.model)\n return opts.model;\n return null;\n}\nfunction specDegradation(spec, opts, env) {\n if (!spec)\n return null;\n const parts = String(spec).split(':');\n const head = parts[0] || '';\n if (head === 'codex') {\n const requested = parts[1] || env.CODEX_MODEL;\n if (opts.codexModel !== requested)\n return 'codex-id-substituted';\n return null;\n }\n if (!CLAUDE_NAMES[head])\n return 'spec-unrecognised';\n return null;\n}\nfunction decisionFor(base, spec, opts, env) {\n const deg = specDegradation(spec, opts, env);\n if (deg !== null)\n return { opts: opts, spec: effectiveSpec(opts), reason: deg };\n return { opts: opts, spec: effectiveSpec(opts), reason: base };\n}\nfunction decideFromSpec(base, stage, spec, env) {\n if (stage === 'code' && (spec === null || spec === undefined)) {\n const s = resolveCoderSpec(env);\n return decisionFor(base, s, specToOpts(s, env), env);\n }\n if (stage === 'qe' && (spec === null || spec === undefined)) {\n const s = resolveQeSpec(env);\n return decisionFor(base, s, specToOpts(s, env), env);\n }\n return decisionFor(base, spec, specToOpts(spec, env), env);\n}\nfunction resolveStageDecision(stage, env) {\n if (env.usageOverride) {\n const r = (env.usageReasoning && env.usageReasoning[stage]) || STAGE_EFFORT.override[stage] || 'medium';\n const s = 'codex:' + topCodexId(env) + ':' + r;\n const o = specToOpts(s, env);\n o._usageSwitched = true;\n return decisionFor('usage-override', s, o, env);\n }\n const spec = env.MODELS[stage];\n if (spec !== undefined)\n return decideFromSpec('explicit-models', stage, spec, env);\n if (!routingRequested(env))\n return { opts: {}, spec: null, reason: 'routing-not-requested' };\n if (stage === 'code' && (env.CODER === 'codex' || env.CODER === 'codex-fallback')) {\n const s = resolveCoderSpec(env);\n return decisionFor('coder-knob-codex', s, specToOpts(s, env), env);\n }\n if (stage === 'plan' && env.PLANNER === 'codex') {\n const s = 'codex:' + env.CODEX_MODEL + ':high';\n return decisionFor('planner-knob-codex', s, specToOpts(s, env), env);\n }\n if (stage === 'qe') {\n const s = resolveQeSpec(env);\n return decisionFor(qeReasonForFamilies(coderIsCodex(env), s), s, specToOpts(s, env), env);\n }\n const resolvedPrimary = env.primary || 'claude';\n const cell = budgetTable(resolvedPrimary, resolveBudgetMode(env.budget), env)[stage];\n if (cell !== undefined)\n return decideFromSpec('budget-table-cell', stage, cell, env);\n return decideFromSpec('default-models', stage, DEFAULT_MODELS[stage], env);\n}\nfunction modelLabel(opts) {\n if (opts && opts.agentType === 'codex:codex-rescue') {\n const base = 'codex:' + opts.codexModel + ':' + opts._reasoning;\n return opts._usageSwitched ? base + ' (usage-switched)' : base;\n }\n if (opts && opts.model)\n return opts.model;\n return 'session';\n}\nfunction stageLabel(base, opts) {\n const m = modelLabel(opts);\n return m === 'session' ? base : base + ' · ' + m;\n}\nfunction mergeOpts(base, extra) {\n const out = {};\n for (const k in base)\n out[k] = base[k];\n for (const k in extra)\n out[k] = extra[k];\n return out;\n}",
69
69
  },
70
70
  "usage-probes": {
71
71
  name: "usage-probes",
@@ -94,6 +94,15 @@ export const BLOBS: Record<string, LoopBlob> = {
94
94
  exports: ["CHALLENGE_QUESTIONS","CHALLENGE_VERDICT_SCHEMA","buildChallengeBrief","sanitizeFinding","sanitizeVerdict","findingsNeedingCrossValidation","confirmedVerdict","renderVerdict","pickAdversaryModel","classifyModelFamily"],
95
95
  code: "const SEV_RANK = { P0: 3, P1: 2, P2: 1 };\nconst VALID_CID = new Set(['C1', 'C2', 'C3', 'C4', 'C5', 'C6', 'C7', 'C8']);\nconst isValidSeverity = (s) => s === 'P0' || s === 'P1' || s === 'P2';\nconst CHALLENGE_QUESTIONS = Object.freeze([\n Object.freeze({\n id: 'C1',\n title: 'Architecture anti-cement',\n prompt: 'Does this plan cement a NEW bad pattern, a wrong boundary, or a shortcut that later work will be ' +\n 'forced to copy? Check it against the product map + vision. IMPORTANT: deviating from a pattern that ' +\n 'is REGISTERED in the accepted-degradations registry is NOT a finding — that debt is already owned. ' +\n 'A genuinely new degradation the plan introduces → name it and propose it for the registry.',\n }),\n Object.freeze({\n id: 'C2',\n title: 'Production-ready',\n prompt: 'Where would this fall over in production? Missing error handling, unhandled failure modes, ' +\n 'resource leaks, missing observability, config/secret handling, migration/rollback. Name the ' +\n 'concrete input or condition that breaks it, not a general worry.',\n }),\n Object.freeze({\n id: 'C3',\n title: 'Test sufficiency + honesty (both ways)',\n prompt: 'Attack the test plan from BOTH sides. Under-testing: which claim — especially a safety property the ' +\n 'ADR NAMES (\"never X\") — has no falsifying test? A test that cannot fail (a wrapper, a tautology, ' +\n 'asserting the mock) is not coverage. Over-testing: which tests are theater — restating the ' +\n 'implementation, testing the framework, brittle snapshots that verify nothing a user cares about?',\n }),\n Object.freeze({\n id: 'C4',\n title: 'Overengineering sweep',\n prompt: 'What in this plan is built for a requirement nobody stated? Speculative generality, an abstraction ' +\n 'with one caller, a config knob no one asked for, a plugin seam for a single case. For each: what is ' +\n 'the simpler thing that meets the ACTUAL requirement?',\n }),\n Object.freeze({\n id: 'C5',\n title: 'Silent decisions',\n prompt: 'Which load-bearing decisions did the plan make WITHOUT surfacing them as a decision? A default that ' +\n 'is really a policy, a chosen tradeoff presented as the only option, a dependency added in passing. ' +\n 'Each silent decision the owner did not get to refuse is a finding.',\n }),\n Object.freeze({\n id: 'C6',\n title: 'Runtime consistency',\n prompt: 'Will this behave consistently with how the rest of the system already works — same error shape, ' +\n 'same config source, same module/ESM conventions, same logging, same naming? Point to the specific ' +\n 'existing convention the plan contradicts.',\n }),\n Object.freeze({\n id: 'C7',\n title: 'Scope',\n prompt: 'Is the plan more than ~1.5× the size the request actually needs? If so, what is the concrete cut ' +\n 'list — which files/steps/abstractions to drop to hit the real requirement — and what is genuinely ' +\n 'load-bearing and must stay?',\n }),\n Object.freeze({\n id: 'C8',\n title: 'Executability',\n prompt: 'Could an executor who is NOT the plan author complete every step without coming back to ask what was ' +\n 'meant? Find the steps that are under-specified, assume unstated context, or hide a research task ' +\n 'behind an imperative verb (\"integrate X\", \"wire up Y\") with no concrete how.',\n }),\n]);\nconst CHALLENGE_VERDICT_SCHEMA = Object.freeze({\n type: 'object',\n required: ['findings', 'summary'],\n properties: {\n findings: {\n type: 'array',\n items: {\n type: 'object',\n required: ['c', 'severity', 'title', 'why'],\n properties: {\n c: { type: 'string', enum: ['C1', 'C2', 'C3', 'C4', 'C5', 'C6', 'C7', 'C8'] },\n severity: { type: 'string', enum: ['P0', 'P1', 'P2'] },\n title: { type: 'string' },\n why: { type: 'string', description: 'concrete failing input/condition, not a general worry' },\n where: { type: 'string', description: 'plan section / file:line if locatable' },\n },\n },\n },\n summary: { type: 'string' },\n },\n});\nconst HR = '─'.repeat(72);\nconst section = (title, body) => body === undefined ? `## ${title}\\n(not provided — panel runs with less calibration)\\n` : `## ${title}\\n${body}\\n`;\nfunction buildChallengeBrief(ctx) {\n const lines = [];\n lines.push('# CHALLENGE PANEL — adversarial plan-gate');\n lines.push('');\n lines.push('You are a FRESH adversarial reviewer. You did NOT write this plan. Your job is to BREAK it, not to ' +\n 'confirm it — find the concrete way each answer fails, or state plainly that you could not. A finding ' +\n 'is a specific failing input/condition/omission, never a general worry. Every P0/P1 you raise will be ' +\n 'independently cross-validated, so do not pad — theory that cannot be reproduced will be dropped.');\n lines.push('');\n lines.push(HR);\n lines.push(section('PLAN UNDER REVIEW (' + ctx.planPath + ')', ctx.plan === '' ? undefined : ctx.plan));\n lines.push(section('PRODUCT VISION (boundaries + principles)', ctx.vision));\n lines.push(section('TESTING POLICY (what \"done\" + honest tests mean here)', ctx.testing));\n lines.push(section('PRODUCT MAP (subsystems + existing conventions)', ctx.map));\n lines.push(section('ACCEPTED-DEGRADATIONS REGISTRY (deviating from THESE is NOT a finding)', ctx.degradations));\n lines.push(section('CODE HINTS', ctx.codeHints));\n lines.push(HR);\n lines.push('');\n lines.push('## Ask each question in \"break it, don\\'t confirm it\" mode');\n for (const q of CHALLENGE_QUESTIONS) {\n lines.push('');\n lines.push(`### ${q.id} — ${q.title}`);\n lines.push(q.prompt);\n }\n lines.push('');\n lines.push(HR);\n lines.push('## Output');\n lines.push('Return findings tagged by C-number with severity P0 (would ship a serious defect / cements bad ' +\n 'architecture), P1 (real gap, fix before code), or P2 (worth noting). For each: a concrete `why` ' +\n '(the failing input/condition) and `where` if locatable. Then a one-paragraph `summary`. This gate ' +\n 'ADVISES — it does not block; the owner decides.');\n lines.push('');\n lines.push('Verdict JSON schema: ' + JSON.stringify(CHALLENGE_VERDICT_SCHEMA));\n return lines.join('\\n');\n}\nfunction sanitizeFinding(raw) {\n if (raw === null || typeof raw !== 'object')\n return null;\n const r = raw;\n if (typeof r.c !== 'string' || !VALID_CID.has(r.c))\n return null;\n if (!isValidSeverity(r.severity))\n return null;\n if (typeof r.title !== 'string' || r.title === '')\n return null;\n if (typeof r.why !== 'string' || r.why === '')\n return null;\n const out = { c: r.c, severity: r.severity, title: r.title, why: r.why };\n return typeof r.where === 'string' && r.where !== '' ? { ...out, where: r.where } : out;\n}\nfunction sanitizeVerdict(raw) {\n if (raw === null || typeof raw !== 'object')\n return null;\n const r = raw;\n if (!Array.isArray(r.findings))\n return null;\n const findings = r.findings.map(sanitizeFinding).filter((f) => f !== null);\n return { findings, summary: typeof r.summary === 'string' ? r.summary : '' };\n}\nfunction findingsNeedingCrossValidation(v) {\n return [...v.findings]\n .filter((f) => f.severity === 'P0' || f.severity === 'P1')\n .sort(cmpFinding);\n}\nfunction cmpFinding(a, b) {\n const s = SEV_RANK[b.severity] - SEV_RANK[a.severity];\n if (s !== 0)\n return s;\n if (a.c !== b.c)\n return a.c < b.c ? -1 : 1;\n return a.title < b.title ? -1 : a.title > b.title ? 1 : 0;\n}\nfunction confirmedVerdict(v, realFlags) {\n const need = findingsNeedingCrossValidation(v);\n const kept = v.findings.filter((f) => f.severity === 'P2');\n need.forEach((f, i) => {\n if (realFlags[i] === true)\n kept.push({ ...f, crossValidated: true });\n });\n kept.sort(cmpFinding);\n return { findings: kept, summary: v.summary };\n}\nfunction renderVerdict(v) {\n if (v.findings.length === 0) {\n return `Challenge panel: no cross-validated findings. ${v.summary}`.trim();\n }\n const byId = (s) => v.findings.filter((f) => f.severity === s).sort(cmpFinding);\n const out = ['Challenge panel verdict (advisory — you decide):', ''];\n for (const sev of ['P0', 'P1', 'P2']) {\n const group = byId(sev);\n if (group.length === 0)\n continue;\n out.push(`### ${sev} (${group.length})`);\n for (const f of group) {\n const cv = f.crossValidated ? ' ✓cross-validated' : '';\n const where = f.where ? ` [${f.where}]` : '';\n out.push(`- ${f.c} ${f.title}${where}${cv}`);\n out.push(` why: ${f.why}`);\n }\n out.push('');\n }\n out.push(v.summary);\n return out.join('\\n').trim();\n}\nfunction pickAdversaryModel(plannerModel) {\n const fam = classifyModelFamily(plannerModel);\n if (fam === 'claude')\n return { model: 'codex', note: `plan authored on ${plannerModel} (Claude) → Codex adversary (cross-family)` };\n if (fam === 'openai')\n return { model: 'claude', note: `plan authored on ${plannerModel} (OpenAI/Codex) → fresh Claude adversary (cross-family)` };\n return { model: 'claude', note: `plan author family UNKNOWN (${plannerModel}) → Claude adversary by default; verify it is cross-family before trusting the verdict` };\n}\nfunction classifyModelFamily(model) {\n const m = String(model).toLowerCase();\n if (/claude|opus|sonnet|haiku|fable/.test(m))\n return 'claude';\n if (/codex|gpt|openai|\\bo[1-9]\\b/.test(m))\n return 'openai';\n return 'unknown';\n}",
96
96
  },
97
+ "stage-line": {
98
+ name: "stage-line",
99
+ version: "2.0.0",
100
+ contentHash: "3b921b7b8a38edc8ed7ec544eb792e1f14db1872b87d9bcf3c2daa5839c90675",
101
+ sourcePath: "packages/@dzhechkov/harness-core/src/stage-line.ts",
102
+ requires: [],
103
+ exports: ["STAGE_LINE_REASON_LABELS","STAGE_LINE_BARRIER_STAGES","renderStageIntentLine","renderStageOutcomeLine","renderStageLine"],
104
+ code: "const STAGE_LINE_REASON_LABELS = {\n 'usage-override': 'usage override (Claude limit pressure)',\n 'explicit-models': 'explicit args.models',\n 'routing-not-requested': 'routing not requested',\n 'coder-knob-codex': 'coder knob = codex',\n 'planner-knob-codex': 'planner knob = codex',\n 'qe-cross-family': 'cross-family QE (the coder never self-reviews)',\n 'budget-table-cell': 'budget table cell',\n 'default-models': 'default models table',\n 'codex-id-substituted': 'unknown codex id — substituted',\n 'spec-unrecognised': 'unrecognised spec — session-inherited',\n 'coder-fallback': 'coder fallback ladder',\n 'codex-unsupported-at-dispatch': 'codex unsupported at this dispatch — Claude',\n 'fallback-after-no-deliverable': 'fallback — the previous rung delivered nothing',\n 'precision-second-pass': 'independent precision second pass',\n 'auto-cost': 'learned-cost routing',\n 'qe-same-family-degraded': 'same-family QE — cross-family review NOT obtained',\n 'challenge-panel': 'adversarial plan-gate panel',\n 'codex-probe-failed': 'codex probe found no usable id — Claude',\n 'codex-refused-before-dispatch': 'codex refused before dispatching — nothing ran',\n 'fallback-rung': 'fallback rung (prior outcome reported separately)',\n};\nconst STAGE_LINE_BARRIER_STAGES = {\n code: 1,\n plan: 1,\n requirements: 1,\n adr: 1,\n ideation: 1,\n architecture: 1,\n};\nfunction renderStagePrefix(stage, decision) {\n let model = 'session';\n if (decision && decision.spec)\n model = decision.spec;\n return '▸ ' + stage + ' · ' + model;\n}\nfunction renderStageIntentLine(stage, decision) {\n let signature = '';\n if (decision) {\n const labelled = STAGE_LINE_REASON_LABELS[decision.reason];\n if (labelled)\n signature = labelled;\n }\n if (!signature) {\n signature = 'unlabelled branch';\n if (decision && decision.reason)\n signature = 'unlabelled branch: ' + decision.reason;\n }\n let line = renderStagePrefix(stage, decision) + ' · ' + signature;\n if (STAGE_LINE_BARRIER_STAGES[stage] && decision && decision.opts && decision.opts.agentType === 'codex:codex-rescue') {\n line = line + ' · landed barrier';\n }\n return line + ' · intent';\n}\nfunction renderStageOutcomeLine(stage, decision, outcome) {\n let model = 'session';\n if (decision && decision.spec)\n model = decision.spec;\n let label = 'dispatched';\n if (outcome && outcome.state === 'probe-failed')\n label = 'probe found no usable model';\n else if (outcome && outcome.state === 'refused-before-dispatch')\n label = 'refused before dispatch';\n let line = '◆ ' + stage + ' · ' + model + ' · outcome: ' + label;\n if (outcome && outcome.state === 'refused-before-dispatch' && outcome.reason === 'codex-unsupported-at-dispatch') {\n line = line + ' · codex agent type unsupported';\n }\n else if (outcome && outcome.state === 'refused-before-dispatch' && outcome.reason) {\n line = line + ' · reason: ' + outcome.reason;\n }\n return line;\n}\nfunction renderStageLine(stage, decision) {\n const intent = renderStageIntentLine(stage, decision);\n return intent.slice(0, intent.length - ' · intent'.length);\n}",
105
+ },
97
106
  "trace": {
98
107
  name: "trace",
99
108
  version: "1.1.0",
package/src/loop-lint.ts CHANGED
@@ -22,7 +22,7 @@
22
22
  */
23
23
 
24
24
  import { createHash } from 'node:crypto';
25
- import { toLintProjection, JOIN_POLICIES, QUORUM_RE, type LoopPlan, type LintProjection } from './loop-plan.js';
25
+ import { toLintProjection, stepIdent, JOIN_POLICIES, QUORUM_RE, type LoopPlan, type LintProjection } from './loop-plan.js';
26
26
  import type { LoopBlob } from './loop-blobs.generated.js';
27
27
 
28
28
  export type LintVerdict = 'pass' | 'fail' | 'inconclusive';
@@ -584,6 +584,29 @@ export const TOOL_PERIMETER_ENTRY_RE = /^[a-z][a-z0-9-]*(:[a-z][a-z0-9-]*)+$/;
584
584
  * exactly where cfr-pipeline runs it. Plan-less scripts report `inconclusive` (`no-plan-binding`)
585
585
  * like every other plan-anchored rule — never a silent pass.
586
586
  */
587
+ /**
588
+ * Текст массива промпта одного шага: содержимое `const P_<ident> = [ … ]` со сбалансированными
589
+ * скобками. `null`, если такого массива в скрипте нет.
590
+ *
591
+ * Скобки считаются, а не ищется первая `]`: строки договора содержат запятые и текст, а соседний
592
+ * шаг начинается сразу за концом массива — обрыв по первой скобке склеил бы соседей.
593
+ */
594
+ function promptArrayRegion(code: string, ident: string): string | null {
595
+ const head = new RegExp(`const\\s+P_${ident.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\s*=\\s*\\[`).exec(code);
596
+ if (head === null) return null;
597
+ const open = head.index + head[0].length - 1;
598
+ let depth = 0;
599
+ for (let i = open; i < code.length; i += 1) {
600
+ const ch = code[i];
601
+ if (ch === '[') depth += 1;
602
+ else if (ch === ']') {
603
+ depth -= 1;
604
+ if (depth === 0) return code.slice(open + 1, i);
605
+ }
606
+ }
607
+ return null; // массив не закрыт — область не установлена, и выдумывать её нельзя
608
+ }
609
+
587
610
  function ruleToolPerimeterDeclared(ctx: Ctx): LintFinding[] {
588
611
  if (ctx.projection === null) return noPlan('tool-perimeter-declared');
589
612
  const sev: LintSeverity = ctx.mode === 'require-plan' ? 'fail' : 'warn';
@@ -604,13 +627,35 @@ function ruleToolPerimeterDeclared(ctx: Ctx): LintFinding[] {
604
627
  }
605
628
  seen.add(entry);
606
629
  }
607
- // script cross-check: the rendered contract line must agree with the DECLARED array — a plan
608
- // edit that never re-rendered would otherwise pass on the plan half alone.
609
- if (f.tools.length > 0) {
610
- const expected = 'declared MCP tool allowlist (plan tools): ' + f.tools.join(', ') + ' — use NOTHING outside it.';
611
- if (!ctx.script.includes(expected)) {
612
- out.push({ rule: 'tool-perimeter-declared', severity: sev, message: `step ${f.id}: the rendered script carries no contract line matching the declared perimeter [${f.tools.join(', ')}] — the script is stale against the plan, or the perimeter is decorative`, anchor: f.id });
630
+ // ПЕРЕКРЁСТНАЯ ПРОВЕРКА СО СКРИПТОМ — В ОБЛАСТИ ЭТОГО ШАГА И БЕЗ КОММЕНТАРИЕВ.
631
+ //
632
+ // Три слабости прежней редакции, все ПОДТВЕРЖДЕНЫ ревью (бэклог 6b351420) и воспроизведены:
633
+ // (а) проверка шла только при `tools.length > 0` — сжатие периметра до `[]` без ре-рендера
634
+ // оставляло в скрипте СТАРУЮ строку договора, и правило молчало;
635
+ // (б) `ctx.script.includes` искал ГЛОБАЛЬНО — потерянная строка одного шага удовлетворялась
636
+ // одинаковым периметром ДРУГОГО шага;
637
+ // (в) искали в `ctx.script`, а не в `ctx.code`, поэтому строка договора, перенесённая в
638
+ // `//`-комментарий, проходила как настоящая.
639
+ //
640
+ // ОБЛАСТЬ ШАГА — ЭТО ЕГО МАССИВ ПРОМПТА `const P_<ident> = [ … ]`, а НЕ область между
641
+ // маркерами `BEGIN/END step:<id>`. Измерено на отрендеренном плане: строка договора шага
642
+ // `lane`, раздаваемого веером, лежит в области шага `fan` — привязка к маркерам дала бы
643
+ // ложное нарушение на каждом веерном шаге.
644
+ const PERIMETER_PREFIX = 'declared MCP tool allowlist (plan tools): ';
645
+ const region = promptArrayRegion(ctx.code, stepIdent(f.id));
646
+ if (region === null && f.tools.length > 0) {
647
+ // Периметр ОБЪЯВЛЕН, а сверять его не с чем: у шага нет массива промпта в скрипте. Для шага
648
+ // с ПУСТЫМ периметром отсутствие области — не находка: противоречить там нечему, и краснеть
649
+ // на всяком ненарендеренном скрипте значило бы изобретать нарушения.
650
+ out.push({ rule: 'tool-perimeter-declared', severity: sev, message: `step ${f.id}: the script carries no \`const P_${stepIdent(f.id)} = [ … ]\` prompt array — a declared perimeter [${f.tools.join(', ')}] cannot be cross-checked against it at all`, anchor: f.id });
651
+ } else if (region !== null && f.tools.length > 0) {
652
+ const expected = PERIMETER_PREFIX + f.tools.join(', ') + ' — use NOTHING outside it.';
653
+ if (!region.includes(expected)) {
654
+ out.push({ rule: 'tool-perimeter-declared', severity: sev, message: `step ${f.id}: the step's own prompt carries no contract line matching the declared perimeter [${f.tools.join(', ')}] — the script is stale against the plan, or the perimeter is decorative`, anchor: f.id });
613
655
  }
656
+ } else if (region !== null && region.includes(PERIMETER_PREFIX)) {
657
+ // Периметр сжали до пустого, а строка договора осталась: скрипт разрешает больше, чем план.
658
+ out.push({ rule: 'tool-perimeter-declared', severity: sev, message: `step ${f.id}: the plan declares an EMPTY tool perimeter, but the step's prompt still carries a tool-allowlist contract line — the script was not re-rendered and grants more than the plan allows`, anchor: f.id });
614
659
  }
615
660
  }
616
661
  return out;