tickmarkr 2.6.1 → 2.6.2

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 (85) hide show
  1. package/README.md +14 -3
  2. package/dist/adapters/catalog-remote.js +89 -47
  3. package/dist/adapters/claude-code.js +9 -6
  4. package/dist/adapters/codex.js +7 -4
  5. package/dist/adapters/prompt.d.ts +1 -0
  6. package/dist/adapters/prompt.js +14 -6
  7. package/dist/adapters/registry.js +3 -3
  8. package/dist/adapters/types.d.ts +12 -4
  9. package/dist/adapters/types.js +6 -0
  10. package/dist/cli/commands/approve.d.ts +5 -1
  11. package/dist/cli/commands/approve.js +66 -23
  12. package/dist/cli/commands/compile.js +13 -3
  13. package/dist/cli/commands/doctor.d.ts +2 -0
  14. package/dist/cli/commands/doctor.js +11 -3
  15. package/dist/cli/commands/fleet.js +45 -7
  16. package/dist/cli/commands/report.js +18 -2
  17. package/dist/cli/commands/resume.js +4 -2
  18. package/dist/cli/commands/status.js +24 -19
  19. package/dist/cli/help.d.ts +2 -0
  20. package/dist/cli/help.js +9 -2
  21. package/dist/config/config.d.ts +20 -0
  22. package/dist/config/config.js +47 -8
  23. package/dist/config/fleet-overlay.d.ts +1 -0
  24. package/dist/config/fleet-overlay.js +56 -0
  25. package/dist/drivers/orca.d.ts +26 -1
  26. package/dist/drivers/orca.js +193 -60
  27. package/dist/eval/canary.d.ts +2 -1
  28. package/dist/eval/canary.js +2 -2
  29. package/dist/eval/dispatch.js +1 -0
  30. package/dist/gates/acceptance.d.ts +2 -1
  31. package/dist/gates/acceptance.js +7 -2
  32. package/dist/gates/baseline.d.ts +12 -1
  33. package/dist/gates/baseline.js +11 -4
  34. package/dist/gates/llm.d.ts +5 -4
  35. package/dist/gates/llm.js +13 -13
  36. package/dist/gates/review.d.ts +8 -0
  37. package/dist/gates/review.js +40 -4
  38. package/dist/gates/run-gates.d.ts +2 -1
  39. package/dist/gates/run-gates.js +27 -13
  40. package/dist/gates/test-manifest.d.ts +3 -1
  41. package/dist/gates/test-manifest.js +9 -2
  42. package/dist/graph/schema.d.ts +2 -0
  43. package/dist/graph/schema.js +2 -0
  44. package/dist/plan/scope.js +2 -2
  45. package/dist/route/preference.d.ts +20 -2
  46. package/dist/route/preference.js +48 -13
  47. package/dist/route/router.js +30 -15
  48. package/dist/run/consult.d.ts +13 -1
  49. package/dist/run/consult.js +14 -5
  50. package/dist/run/daemon.d.ts +37 -2
  51. package/dist/run/daemon.js +579 -141
  52. package/dist/run/git.d.ts +8 -0
  53. package/dist/run/git.js +14 -0
  54. package/dist/run/journal.d.ts +126 -3
  55. package/dist/run/journal.js +410 -37
  56. package/dist/run/merge.d.ts +3 -1
  57. package/dist/run/merge.js +3 -2
  58. package/dist/run/operator-summary.d.ts +3 -0
  59. package/dist/run/operator-summary.js +3 -1
  60. package/dist/run/protocol.d.ts +31 -1
  61. package/dist/run/protocol.js +3 -1
  62. package/dist/run/supervision.d.ts +7 -1
  63. package/dist/run/supervision.js +5 -2
  64. package/dist/tui/cockpit/board.js +3 -3
  65. package/dist/tui/cockpit/decision-actions.d.ts +8 -5
  66. package/dist/tui/cockpit/decision-actions.js +55 -32
  67. package/dist/tui/cockpit/derive.js +13 -2
  68. package/dist/tui/cockpit/live-runtime.d.ts +10 -0
  69. package/dist/tui/cockpit/live-runtime.js +50 -3
  70. package/dist/tui/cockpit/run-cockpit.d.ts +3 -0
  71. package/dist/tui/cockpit/run-cockpit.js +26 -1
  72. package/dist/tui/cockpit/run-view.d.ts +9 -3
  73. package/dist/tui/cockpit/run-view.js +60 -7
  74. package/dist/tui/cockpit/setup-cockpit.d.ts +4 -0
  75. package/dist/tui/cockpit/setup-cockpit.js +6 -3
  76. package/dist/tui/ink/fleet-app.d.ts +15 -3
  77. package/dist/tui/ink/fleet-app.js +91 -22
  78. package/package.json +2 -1
  79. package/schema/config.schema.json +818 -0
  80. package/skills/tickmarkr-loop/SKILL.md +8 -2
  81. package/skills/tickmarkr-overseer/SKILL.md +42 -0
  82. package/skills/tickmarkr-overseer/scripts/classify-vitest-log.sh +88 -0
  83. package/skills/tickmarkr-overseer/scripts/context-statusline.sh +81 -0
  84. package/skills/tickmarkr-overseer/scripts/grade-ci.sh +36 -34
  85. package/skills/tickmarkr-overseer/scripts/watch-journal.sh +6 -4
@@ -1,5 +1,5 @@
1
1
  import type { TickmarkrConfig } from "../config/config.js";
2
- import { type Baseline, type GateEvidenceOptions, type FailureClassification } from "../gates/baseline.js";
2
+ import { type Baseline, type BaselineProvenance, type GateEvidenceOptions, type FailureClassification } from "../gates/baseline.js";
3
3
  import type { GateEvidenceReceipt } from "./protocol.js";
4
4
  export interface TipVerifyResult {
5
5
  evidenceReceipt?: GateEvidenceReceipt;
@@ -21,6 +21,8 @@ export interface TipVerifyResult {
21
21
  spawnedCommand?: string;
22
22
  /** Q121s: nonzero exit whose failures are ALL baseline-recorded — forgiven exactly as the battery forgives. */
23
23
  forgiven?: boolean;
24
+ /** OBS-1123: the capture that recorded the forgiven fingerprints; absent for a legacy baseline that never said. */
25
+ baselineProvenance?: BaselineProvenance;
24
26
  /** D-131: carried from a persisted per-gate verdict — this cycle did NOT execute the command. */
25
27
  reused?: true;
26
28
  /**
package/dist/run/merge.js CHANGED
@@ -225,6 +225,7 @@ export async function verifyIntegrationTip(intWt, commands, runDir, baseline, ev
225
225
  exitCode: hit.exitCode ?? 0,
226
226
  fingerprints: hit.meta?.fingerprints ?? [],
227
227
  ...(hit.meta?.forgiven ? { forgiven: true } : {}),
228
+ ...(hit.meta?.forgiven && hit.meta.baselineProvenance ? { baselineProvenance: hit.meta.baselineProvenance } : {}),
228
229
  details: formatReusedDetails(hit.details, id),
229
230
  ...(hit.meta?.reportPath ? { reportPath: hit.meta.reportPath } : {}),
230
231
  ...(hit.meta?.spawnedCommand ? { spawnedCommand: hit.meta.spawnedCommand } : {}),
@@ -382,7 +383,7 @@ export async function verifyIntegrationTip(intWt, commands, runDir, baseline, ev
382
383
  + `${describeCapacity(entry?.capacity)} and this verification ran under ${describeCapacity(r.capacity)} `
383
384
  + `— forgiveness across a changed capacity is not evidence`
384
385
  : `exit ${r.code}`)),
385
- ...(forgiven && !deficit ? { forgiven: true } : {}),
386
+ ...(forgiven && !deficit ? { forgiven: true, ...(baseline?.provenance ? { baselineProvenance: baseline.provenance } : {}) } : {}),
386
387
  ...(cause ? { cause } : {}),
387
388
  ...(pass ? {} : { artifact }),
388
389
  };
@@ -401,7 +402,7 @@ export async function verifyIntegrationTip(intWt, commands, runDir, baseline, ev
401
402
  const id = pending.get(result.gate);
402
403
  if (id && id.tree === completedTree && !isInfraResult(result)) {
403
404
  store.set(id, { ...result, meta: {
404
- fingerprints: result.fingerprints, forgiven: result.forgiven,
405
+ fingerprints: result.fingerprints, forgiven: result.forgiven, baselineProvenance: result.baselineProvenance,
405
406
  reportPath: result.reportPath, spawnedCommand: result.spawnedCommand,
406
407
  artifact: result.artifact,
407
408
  evidenceReceipt: result.evidenceReceipt, evidenceReceipts: result.evidenceReceipts,
@@ -13,6 +13,9 @@ export interface OperatorDecisionSnapshot {
13
13
  readonly kind: string | undefined;
14
14
  readonly tombstone: boolean;
15
15
  readonly reason?: string;
16
+ /** OBS-1178: the park row's physical line and timestamp — the token a decision binds to. */
17
+ readonly line?: number;
18
+ readonly ts?: string;
16
19
  };
17
20
  readonly verbs: readonly string[];
18
21
  readonly diagnostic?: string;
@@ -25,7 +25,9 @@ export function projectOperatorSummary(tasks, decisions) {
25
25
  decisionRequired: verbs.length > 0,
26
26
  ...(diagnostic !== undefined || park.reason !== undefined
27
27
  ? { diagnostic: diagnostic ?? park.reason } : {}),
28
- nextAction: verbs.length > 0 ? `Choose a decision: ${verbs.join(", ")}` : null,
28
+ nextAction: verbs.length > 0
29
+ ? `Choose a decision: ${verbs.join(", ")}${park.line !== undefined && park.ts !== undefined ? ` (--park ${park.line}@${park.ts})` : ""}`
30
+ : null,
29
31
  };
30
32
  if (!park.tombstone && park.kind === "infra" && task.missingCapability) {
31
33
  const { capability, diagnostic: capabilityDiagnostic } = task.missingCapability;
@@ -66,10 +66,10 @@ export declare const GateEvidenceReceiptSchema: z.ZodObject<{
66
66
  termination: z.ZodObject<{
67
67
  kind: z.ZodEnum<{
68
68
  unknown: "unknown";
69
+ timeout: "timeout";
69
70
  signal: "signal";
70
71
  "not-started": "not-started";
71
72
  exit: "exit";
72
- timeout: "timeout";
73
73
  }>;
74
74
  exitCode: z.ZodNullable<z.ZodNumber>;
75
75
  signal: z.ZodNullable<z.ZodString>;
@@ -222,6 +222,11 @@ export declare const RoleInvocationStartEventSchema: z.ZodObject<{
222
222
  adapter: z.ZodOptional<z.ZodString>;
223
223
  model: z.ZodOptional<z.ZodString>;
224
224
  vendor: z.ZodOptional<z.ZodString>;
225
+ effort: z.ZodOptional<z.ZodEnum<{
226
+ low: "low";
227
+ medium: "medium";
228
+ high: "high";
229
+ }>>;
225
230
  evidence: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
226
231
  }, z.core.$strict>;
227
232
  }, z.core.$strict>;
@@ -240,6 +245,11 @@ export declare const RoleInvocationTerminalEventSchema: z.ZodObject<{
240
245
  adapter: z.ZodOptional<z.ZodString>;
241
246
  model: z.ZodOptional<z.ZodString>;
242
247
  vendor: z.ZodOptional<z.ZodString>;
248
+ effort: z.ZodOptional<z.ZodEnum<{
249
+ low: "low";
250
+ medium: "medium";
251
+ high: "high";
252
+ }>>;
243
253
  evidence: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
244
254
  outcome: z.ZodEnum<{
245
255
  failed: "failed";
@@ -352,6 +362,11 @@ export declare const DecisionEventSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
352
362
  adapter: z.ZodOptional<z.ZodString>;
353
363
  model: z.ZodOptional<z.ZodString>;
354
364
  vendor: z.ZodOptional<z.ZodString>;
365
+ effort: z.ZodOptional<z.ZodEnum<{
366
+ low: "low";
367
+ medium: "medium";
368
+ high: "high";
369
+ }>>;
355
370
  evidence: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
356
371
  }, z.core.$strict>;
357
372
  }, z.core.$strict>, z.ZodObject<{
@@ -369,6 +384,11 @@ export declare const DecisionEventSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
369
384
  adapter: z.ZodOptional<z.ZodString>;
370
385
  model: z.ZodOptional<z.ZodString>;
371
386
  vendor: z.ZodOptional<z.ZodString>;
387
+ effort: z.ZodOptional<z.ZodEnum<{
388
+ low: "low";
389
+ medium: "medium";
390
+ high: "high";
391
+ }>>;
372
392
  evidence: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
373
393
  outcome: z.ZodEnum<{
374
394
  failed: "failed";
@@ -473,6 +493,11 @@ export declare const DecisionEventWriteSchema: z.ZodDiscriminatedUnion<[z.ZodObj
473
493
  adapter: z.ZodOptional<z.ZodString>;
474
494
  model: z.ZodOptional<z.ZodString>;
475
495
  vendor: z.ZodOptional<z.ZodString>;
496
+ effort: z.ZodOptional<z.ZodEnum<{
497
+ low: "low";
498
+ medium: "medium";
499
+ high: "high";
500
+ }>>;
476
501
  evidence: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
477
502
  }, z.core.$strict>;
478
503
  event: z.ZodLiteral<"role-invocation-start">;
@@ -489,6 +514,11 @@ export declare const DecisionEventWriteSchema: z.ZodDiscriminatedUnion<[z.ZodObj
489
514
  adapter: z.ZodOptional<z.ZodString>;
490
515
  model: z.ZodOptional<z.ZodString>;
491
516
  vendor: z.ZodOptional<z.ZodString>;
517
+ effort: z.ZodOptional<z.ZodEnum<{
518
+ low: "low";
519
+ medium: "medium";
520
+ high: "high";
521
+ }>>;
492
522
  evidence: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
493
523
  outcome: z.ZodEnum<{
494
524
  failed: "failed";
@@ -1,5 +1,5 @@
1
1
  import { z } from "zod";
2
- import { GATE_NAMES } from "../graph/schema.js";
2
+ import { EFFORTS, GATE_NAMES } from "../graph/schema.js";
3
3
  import { normalizeGateOutcome } from "./outcome.js";
4
4
  // T32 (F2): the decision protocol is deliberately smaller than the journal. Existing journals contain
5
5
  // many observational event types and must remain readable; only the five event names below assert a
@@ -133,6 +133,8 @@ const RoleInvocationIdentityDataSchema = z.object({
133
133
  adapter: NonEmptyStringSchema.optional(),
134
134
  model: NonEmptyStringSchema.optional(),
135
135
  vendor: NonEmptyStringSchema.optional(),
136
+ // OBS-1182: the effort the seat actually launched at; absent = the CLI's own default, never a level.
137
+ effort: z.enum(EFFORTS).optional(),
136
138
  evidence: EvidenceSchema.optional(),
137
139
  });
138
140
  const RoleInvocationStartDataSchema = RoleInvocationIdentityDataSchema.strict();
@@ -48,6 +48,7 @@ export interface SupervisionArm {
48
48
  export declare const supervisionArmPath: (repoRoot: string, tier: SupervisionTier) => string;
49
49
  /** Explicit arming acknowledges the current marker without deleting it. */
50
50
  export declare function newSupervisionArm(repoRoot: string, tier: SupervisionTier, armId?: string, initial?: boolean): SupervisionArm;
51
+ export declare const supervisionPresencePath: (repoRoot: string, tier: SupervisionTier, id: string) => string;
51
52
  export declare function beatSupervision(repoRoot: string, tier: SupervisionTier, seat?: string, observation?: SupervisionBeatObservation, options?: {
52
53
  arm?: SupervisionArm;
53
54
  armId?: string;
@@ -69,7 +70,10 @@ export declare function supervisionStatus(repoRoot: string, tier: SupervisionTie
69
70
  export declare const readSupervision: (repoRoot: string, now?: number) => TierLiveness[];
70
71
  /** One line, one word per tier. Shared by both status surfaces so neither can render a state twice. */
71
72
  export declare const supervisionText: (tiers: readonly TierLiveness[], divider?: string) => string;
72
- /** A board's durable identity is independent of its short title and of other observers. */
73
+ /** A board's durable identity is independent of its short title and of other observers.
74
+ * `handle` is the terminal the claiming observer reports running in, when its host exports one: the
75
+ * one fact that names the board's pane when the placing split's receipt never arrived (OBS-1172).
76
+ * `retired` marks a tombstone a driver keeps in the record's place; it is never answered again. */
73
77
  export interface WatchBoardOwner {
74
78
  repo: string;
75
79
  runId: string;
@@ -80,6 +84,8 @@ export interface WatchBoardOwner {
80
84
  token: string;
81
85
  pid?: number;
82
86
  armId?: string;
87
+ handle?: string;
88
+ retired?: true;
83
89
  }
84
90
  export declare const WATCH_OWNER_ENV = "TICKMARKR_WATCH_OWNER";
85
91
  export declare function readWatchBoard(repo: string, runId: string): WatchBoardOwner | undefined;
@@ -154,7 +154,7 @@ const supervisionObligationPath = (repoRoot, tier) => join(supervisionDir(repoRo
154
154
  // Freshness decides presence, never a process table (SUP-02): a watcher that is killed cannot remove
155
155
  // its own file, and an unremoved file ages past the same ceiling a beat does and stops counting.
156
156
  const presencePrefix = (tier) => `${tier}.live.`;
157
- const supervisionPresencePath = (repoRoot, tier, id) => join(supervisionDir(repoRoot), `${presencePrefix(tier)}${id}`);
157
+ export const supervisionPresencePath = (repoRoot, tier, id) => join(supervisionDir(repoRoot), `${presencePrefix(tier)}${id}`);
158
158
  /** Every presence file on this tier, by name. Missing directory ⇒ nobody is present. */
159
159
  const presenceNames = (repoRoot, tier) => {
160
160
  try {
@@ -567,6 +567,8 @@ export const supervisionText = (tiers, divider = " · ") => `supervision: ${tier
567
567
  `${t.clearOwedSince ? ` CLEAR-OWED since ${t.clearOwedSince}` : ""}` +
568
568
  `${t.clearOwedUnreadable ? " CLEAR-OWED unreadable" : ""}`).join(divider)}`;
569
569
  export const WATCH_OWNER_ENV = "TICKMARKR_WATCH_OWNER";
570
+ /** Orca exports each terminal's own handle into the processes it runs. */
571
+ const TERMINAL_HANDLE_ENV = "ORCA_TERMINAL_HANDLE";
570
572
  const boardPath = (repo, runId) => join(supervisionDir(repo), `watch-board.${parseRunId(runId)}.json`);
571
573
  const boardMessagePath = (owner, kind) => {
572
574
  if (!/^[a-f0-9-]{36}$/.test(owner.token))
@@ -642,7 +644,8 @@ export function observeNamedRun(repo, runId, env = process.env) {
642
644
  const candidate = readWatchBoard(repo, runId);
643
645
  if (!candidate || candidate.token !== token || candidate.pid !== undefined)
644
646
  throw new Error("watch ownership unknown; refusing narrator acknowledgement");
645
- owner = { ...candidate, pid: process.pid, armId: armed.id };
647
+ const handle = env[TERMINAL_HANDLE_ENV]?.trim();
648
+ owner = { ...candidate, pid: process.pid, armId: armed.id, ...(handle ? { handle } : {}) };
646
649
  atomicRecord(boardPath(repo, runId), owner);
647
650
  }
648
651
  }
@@ -47,9 +47,9 @@ const AREAS = [
47
47
  ["FLEET", [/^src\/(route|config|adapters)\//]],
48
48
  ["RUN", [/^src\/(run|drivers|eval)\//]],
49
49
  ["GATE", [/^src\/gates\//]],
50
- ["UI", [/^src\/(cli|tui|report)\//, /^src\/brand\.ts/]],
51
- ["DOCS", [/^(docs|skills)\//]],
52
- ["REPO", [/^(scripts|schema)\//, /^\.github\//, /^tests\/(lint|repo|helpers)\//, /^(vitest\.config|package\.json|tsconfig)/]],
50
+ ["UI", [/^src\/(cli|tui|report)\//, /^src\/brand\.ts/, /^tests\/cockpit\//]],
51
+ ["DOCS", [/^(docs|skills)\//, /^\.claude\/skills\//]],
52
+ ["REPO", [/^(scripts|schema)\//, /^\.github\//, /^tests\/(lint|repo|helpers|fixtures)\//, /^(vitest\.config|package\.json|tsconfig)/]],
53
53
  ];
54
54
  export const BOARD_AREA_ORDER = AREAS.map(([n]) => n);
55
55
  const expandBraces = (p) => {
@@ -25,6 +25,10 @@ export interface DecisionCommand {
25
25
  readonly reason?: string;
26
26
  /** `--review-rounds`: the review-round ceiling the release carries. */
27
27
  readonly reviewRounds?: number;
28
+ /** OBS-1178: `--park <line>@<ts>` — the displayed park this decision binds to. */
29
+ readonly park?: string;
30
+ /** OBS-1178: `--gate <gate>` — the failed gate a waive binds to. */
31
+ readonly gate?: string;
28
32
  }
29
33
  /** The exact argv the production command receives — built only from the closed verb set. */
30
34
  export declare function decisionArgv(command: DecisionCommand, { runId, by }: {
@@ -57,13 +61,11 @@ export declare const journalFileFor: (cwd: string, runId: string) => string;
57
61
  * the named verb is outside the park's table, so a confirm inset can never promise a decision the
58
62
  * command would refuse. The enactor is read from the run lock now, and read again at the receipt.
59
63
  */
60
- export declare function previewDecision(command: DecisionCommand, { cwd, runId, by }: {
64
+ export declare function previewDecision(named: DecisionCommand, { cwd, runId, by }: {
61
65
  cwd: string;
62
66
  runId: string;
63
67
  by: string;
64
68
  }): DecisionPreviewResult;
65
- /** Who enacts it — the three lock states, each stated by the command's own sentence. */
66
- export declare function enactmentLines(run: ApprovalRunOwner, disposition: ApprovalDisposition): string[];
67
69
  /** The confirm inset: run/task, the newest park line, actor/reason, exact argv, consequence, enactor. */
68
70
  export declare function decisionConfirmLines(preview: DecisionPreview): readonly string[];
69
71
  /** The task-approved row the file gained, located by its physical line. */
@@ -135,8 +137,7 @@ export interface DecisionKeyResult {
135
137
  /**
136
138
  * The reducer never writes and never reads a file; it names commands. `a` opens the menu for the
137
139
  * selected park (a diagnostic park opens a menu with no verbs, stating why); ↑↓ move; Enter picks a
138
- * verb — which OPENS the confirm inset, never confirms; `y` on a confirm names the write; `n`/Esc
139
- * cancel the deepest layer. Enter on a confirm is inert by construction.
140
+ * verb opens the confirm inset; `y` names the write; `n`/Esc cancel the deepest layer. Enter on a confirm is inert.
140
141
  */
141
142
  export declare function applyDecisionKey(session: DecisionSession, event: DecisionKeyEvent, selected: RunDecision | undefined): DecisionKeyResult;
142
143
  /** Store the preview a picked verb produced — or its refusal as the notice. */
@@ -145,3 +146,5 @@ export declare function withDecisionPreview(session: DecisionSession, result: De
145
146
  export declare function withDecisionReceipt(session: DecisionSession, receipt: DecisionReceipt): DecisionSession;
146
147
  /** The verbs the keybar may advertise for the current layer — nothing the flow cannot honour. */
147
148
  export declare function decisionKeybar(session: DecisionSession, selected: RunDecision | undefined): string;
149
+ /** Who enacts it — the three lock states, each stated by the command's own sentence. */
150
+ export declare function enactmentLines(run: ApprovalRunOwner, disposition: ApprovalDisposition): string[];
@@ -1,7 +1,7 @@
1
1
  import { join } from "node:path";
2
- import { APPROVAL_ENACTS, approvalDispositionForRelease, approvalEnactment, approvalRunOwner, approve, DECISION_VERBS, newestPark, permittedDecisionVerbs, readJournalEvents, releaseForDecision, } from "../../cli/commands/approve.js";
2
+ import { APPROVAL_ENACTS, approvalDispositionForRelease, approvalEnactment, approvalRunOwner, approve, DECISION_VERBS, newestPark, parkToken, permittedDecisionVerbs, readJournalEvents, releaseForDecision, } from "../../cli/commands/approve.js";
3
3
  import { stateDirName } from "../../graph/graph.js";
4
- import { Journal } from "../../run/journal.js";
4
+ import { bindingToken, effectiveDecisions, Journal, physicalLine, recordedBinding } from "../../run/journal.js";
5
5
  const parkDiagnostic = (park) => {
6
6
  if (park.kind === "scope-request")
7
7
  return park.approveCommand ?? "scope-request requires tickmarkr approve with --files <glob,…>";
@@ -50,6 +50,8 @@ export function decisionArgv(command, { runId, by }) {
50
50
  const flag = { approve: [], waive: ["--waive"], uphold: ["--uphold"], recheck: ["--recheck"] }[command.verb];
51
51
  return [
52
52
  runId, command.taskId, ...flag,
53
+ ...(command.park === undefined ? [] : ["--park", command.park]),
54
+ ...(command.gate === undefined ? [] : ["--gate", command.gate]),
53
55
  ...(command.reviewRounds === undefined ? [] : ["--review-rounds", String(command.reviewRounds)]),
54
56
  "--by", by,
55
57
  ...(command.reason === undefined ? [] : ["--reason", command.reason]),
@@ -62,10 +64,9 @@ export const journalFileFor = (cwd, runId) => join(stateDirName(cwd), "runs", ru
62
64
  * the named verb is outside the park's table, so a confirm inset can never promise a decision the
63
65
  * command would refuse. The enactor is read from the run lock now, and read again at the receipt.
64
66
  */
65
- export function previewDecision(command, { cwd, runId, by }) {
66
- let argv;
67
+ export function previewDecision(named, { cwd, runId, by }) {
67
68
  try {
68
- argv = decisionArgv(command, { runId, by });
69
+ decisionArgv(named, { runId, by });
69
70
  }
70
71
  catch (e) {
71
72
  return { ok: false, refusal: e.message };
@@ -78,15 +79,29 @@ export function previewDecision(command, { cwd, runId, by }) {
78
79
  catch (e) {
79
80
  return { ok: false, refusal: e.message };
80
81
  }
81
- const park = newestPark(events, command.taskId, sourceIndexes);
82
+ const park = newestPark(events, named.taskId, sourceIndexes);
82
83
  if (!park)
83
- return { ok: false, refusal: `task ${command.taskId} has no park in run ${runId} — nothing to decide` };
84
+ return { ok: false, refusal: `task ${named.taskId} has no park in run ${runId} — nothing to decide` };
85
+ // OBS-1178: the argv binds the park this preview displays; approve refuses it once another opens.
86
+ const token = parkToken(park);
87
+ if (named.park !== undefined && named.park !== token)
88
+ return { ok: false, refusal: `task ${named.taskId}'s open park is ${token ?? "untimestamped"}, not ${named.park} — refresh and decide the open park` };
89
+ if (named.gate !== undefined && named.gate !== park.failedGate)
90
+ return { ok: false, refusal: `refusing mismatched gate for ${named.taskId}: --gate ${named.gate} but park ${token} failed ${park.failedGate ?? "no gate"} — nothing appended` };
91
+ const command = { ...named, ...(token === undefined ? {} : { park: token }), ...(named.verb === "waive" && named.gate === undefined && park.failedGate !== undefined ? { gate: park.failedGate } : {}) };
92
+ let argv;
93
+ try {
94
+ argv = decisionArgv(command, { runId, by });
95
+ }
96
+ catch (e) {
97
+ return { ok: false, refusal: e.message };
98
+ }
84
99
  const verbs = permittedDecisionVerbs(park);
85
100
  if (!verbs.includes(command.verb)) {
86
101
  const why = parkDiagnostic(park) ?? `permitted: ${verbs.join(", ")}`;
87
102
  return { ok: false, refusal: `${command.verb} is not a decision for ${command.taskId}'s ${park.kind ?? "unknown"} park (${why})` };
88
103
  }
89
- const released = events.slice(park.index + 1).find((e) => e.event === "task-approved" && e.taskId === command.taskId);
104
+ const released = releaseSince(events, park.index, command.taskId);
90
105
  if (released) {
91
106
  return { ok: false, refusal: `task ${command.taskId} was already released at #L${sourceIndexes[events.indexOf(released)] + 1} by ${String(released.data.by ?? "unknown")} — nothing to confirm` };
92
107
  }
@@ -100,28 +115,12 @@ export function previewDecision(command, { cwd, runId, by }) {
100
115
  };
101
116
  }
102
117
  const parkLine = (park) => `#L${park.line} ${park.kind ?? "unknown kind"}${park.failedGate ? ` · failed gate ${park.failedGate}` : ""}${park.reason ? ` · ${park.reason}` : ""}`;
103
- const quoteArg = (arg) => (/^[\w.:/@=-]+$/u.test(arg) ? arg : JSON.stringify(arg));
104
- /** What a confirmed write buys, from the command's own table — never a phrase of this file's own. */
105
- function consequenceLines(verb, park, release, disposition) {
106
- const lines = [`consequence disposition ${disposition}; appends one task-approved${release ? ` with release ${release}` : " with no release"}`];
107
- if (verb === "waive")
108
- lines.push(` marks gate ${park.failedGate} satisfied; it does NOT mark the task done`);
109
- if (verb === "recheck")
110
- lines.push(" marks no gate satisfied; the whole declared battery re-runs");
111
- if (verb === "uphold")
112
- lines.push(" sides with the reviewer; funds one fixed attempt carrying the findings");
113
- lines.push(` it will ${APPROVAL_ENACTS[disposition]}`);
114
- return lines;
115
- }
116
- /** Who enacts it — the three lock states, each stated by the command's own sentence. */
117
- export function enactmentLines(run, disposition) {
118
- const owner = run.live
119
- ? "enactment matching live daemon — pending daemon enactment at its next task boundary"
120
- : run.blockingRunId
121
- ? `enactment other live run \`${run.blockingRunId}\` holds the repository lock — recorded, not dispatched`
122
- : `enactment no live owner — recorded, not dispatched; resume required: tickmarkr resume ${run.runId}`;
123
- return [owner, ` ${approvalEnactment(disposition, run)}`];
118
+ /** The decision already open on this park, if any. OBS-1178: a refused or unsound row released nothing. */
119
+ function releaseSince(events, parkIndex, taskId) {
120
+ const effective = effectiveDecisions(events);
121
+ return events.find((e, i) => i > parkIndex && e.event === "task-approved" && e.taskId === taskId && effective.has(physicalLine(events, i)));
124
122
  }
123
+ const quoteArg = (arg) => (/^[\w.:/@=-]+$/u.test(arg) ? arg : JSON.stringify(arg));
125
124
  /** The confirm inset: run/task, the newest park line, actor/reason, exact argv, consequence, enactor. */
126
125
  export function decisionConfirmLines(preview) {
127
126
  return [
@@ -157,7 +156,7 @@ export async function executeDecision(preview, { cwd, command = approve }) {
157
156
  if (!park || park.index !== preview.park.index || park.line !== preview.park.line || park.kind !== preview.park.kind) {
158
157
  return { ok: false, preview, stale: true, refusal: `task ${named.taskId}'s park changed since preview (${park ? `now #L${park.line} ${park.kind ?? "unknown"}` : "no park now"}) — refresh and preview again` };
159
158
  }
160
- const released = before.slice(park.index + 1).find((e) => e.event === "task-approved" && e.taskId === named.taskId);
159
+ const released = releaseSince(before, park.index, named.taskId);
161
160
  if (released) {
162
161
  return { ok: false, preview, stale: true, refusal: `task ${named.taskId} was released at #L${beforeSourceIndexes[before.indexOf(released)] + 1} by ${String(released.data.by ?? "unknown")} after this preview — no second decision appended; refresh` };
163
162
  }
@@ -208,6 +207,7 @@ export async function executeDecision(preview, { cwd, command = approve }) {
208
207
  via: "cli",
209
208
  gate: named.verb === "waive" ? park.failedGate : named.verb === "uphold" ? "review" : undefined,
210
209
  reviewRoundCeiling: named.reviewRounds,
210
+ park: parkToken(park),
211
211
  };
212
212
  const actual = {
213
213
  release,
@@ -216,6 +216,7 @@ export async function executeDecision(preview, { cwd, command = approve }) {
216
216
  via: appended.event.data.via,
217
217
  gate: appended.event.data.gate,
218
218
  reviewRoundCeiling: appended.event.data.reviewRoundCeiling,
219
+ park: recordedBinding(appended.event.data.park) === undefined ? undefined : bindingToken(recordedBinding(appended.event.data.park)),
219
220
  };
220
221
  const mismatch = Object.keys(expected)
221
222
  .find((field) => actual[field] !== expected[field]);
@@ -264,8 +265,7 @@ export const initialDecisionSession = () => ({ menu: null, confirming: null, rec
264
265
  /**
265
266
  * The reducer never writes and never reads a file; it names commands. `a` opens the menu for the
266
267
  * selected park (a diagnostic park opens a menu with no verbs, stating why); ↑↓ move; Enter picks a
267
- * verb — which OPENS the confirm inset, never confirms; `y` on a confirm names the write; `n`/Esc
268
- * cancel the deepest layer. Enter on a confirm is inert by construction.
268
+ * verb opens the confirm inset; `y` names the write; `n`/Esc cancel the deepest layer. Enter on a confirm is inert.
269
269
  */
270
270
  export function applyDecisionKey(session, event, selected) {
271
271
  if (session.confirming !== null) {
@@ -315,3 +315,26 @@ export function decisionKeybar(session, selected) {
315
315
  return session.menu.verbs.length === 0 ? "Esc Close" : "↑↓ Choose · Enter Preview · Esc Close";
316
316
  return selected === undefined ? "" : "a Actions";
317
317
  }
318
+ // Consequence and enactor lines, shared by the confirm inset and the receipt above. Kept below the key
319
+ // grammar so the operator walkthrough's line citations stay put.
320
+ /** What a confirmed write buys, from the command's own table — never a phrase of this file's own. */
321
+ function consequenceLines(verb, park, release, disposition) {
322
+ const lines = [`consequence disposition ${disposition}; appends one task-approved${release ? ` with release ${release}` : " with no release"}`];
323
+ if (verb === "waive")
324
+ lines.push(` marks gate ${park.failedGate} satisfied; it does NOT mark the task done`);
325
+ if (verb === "recheck")
326
+ lines.push(" marks no gate satisfied; the whole declared battery re-runs");
327
+ if (verb === "uphold")
328
+ lines.push(" sides with the reviewer; funds one fixed attempt carrying the findings");
329
+ lines.push(` it will ${APPROVAL_ENACTS[disposition]}`);
330
+ return lines;
331
+ }
332
+ /** Who enacts it — the three lock states, each stated by the command's own sentence. */
333
+ export function enactmentLines(run, disposition) {
334
+ const owner = run.live
335
+ ? "enactment matching live daemon — pending daemon enactment at its next task boundary"
336
+ : run.blockingRunId
337
+ ? `enactment other live run \`${run.blockingRunId}\` holds the repository lock — recorded, not dispatched`
338
+ : `enactment no live owner — recorded, not dispatched; resume required: tickmarkr resume ${run.runId}`;
339
+ return [owner, ` ${approvalEnactment(disposition, run)}`];
340
+ }
@@ -5,6 +5,7 @@ import { channelKey } from "../../adapters/types.js";
5
5
  import { newestPark, permittedDecisionVerbs } from "../../cli/commands/approve.js";
6
6
  import { parseOwnedName } from "../../drivers/types.js";
7
7
  import { projectActivity } from "../../run/activity.js";
8
+ import { effectiveDecisions, withPhysicalLine } from "../../run/journal.js";
8
9
  import { isPidLive } from "../../run/lock.js";
9
10
  import { normalizeGateOutcome } from "../../run/outcome.js";
10
11
  import { projectOperatorSummary } from "../../run/operator-summary.js";
@@ -79,6 +80,15 @@ function parseSource(raw) {
79
80
  }
80
81
  return { events, defects };
81
82
  }
83
+ /**
84
+ * OBS-1178: the rows the task-state folds read. A refused or unsound decision released nothing, so its
85
+ * task-approved row is dropped here; the one decision fold keys every row by its physical line. The
86
+ * journal rows the cockpit draws keep every line as evidence.
87
+ */
88
+ function decidedEvents(events) {
89
+ const effective = effectiveDecisions(events.map((event) => withPhysicalLine(event, event.line)));
90
+ return events.filter((event) => event.event !== "task-approved" || effective.has(event.line));
91
+ }
82
92
  function assignmentFrom(value) {
83
93
  const assignment = recordValue(value);
84
94
  return assignment
@@ -844,7 +854,8 @@ export function deriveRunCockpitData(source, binaryVersion, options = {}) {
844
854
  // alive in the lock — the cockpit called a live run interrupted.
845
855
  : pid !== undefined && (options.isDaemonAlive ?? isPidLive)(pid);
846
856
  const interrupted = !alive;
847
- const tasks = deriveTasks(events, interrupted);
857
+ const decided = decidedEvents(events);
858
+ const tasks = deriveTasks(decided, interrupted);
848
859
  const taskFacts = [...tasks.values()];
849
860
  const done = taskFacts.filter((task) => task.state === "done").length;
850
861
  const failed = taskFacts.filter((task) => task.state === "failed").length;
@@ -873,7 +884,7 @@ export function deriveRunCockpitData(source, binaryVersion, options = {}) {
873
884
  // run-end — the run's own closing statement is enough.
874
885
  const runStatus = failed > 0
875
886
  ? "failed"
876
- : human > 0 || unresolvedSummaryParks(events).length > 0
887
+ : human > 0 || unresolvedSummaryParks(decided).length > 0
877
888
  ? "parked"
878
889
  : lifecycle === "superseded" || hasInterruptedTask
879
890
  ? "interrupted"
@@ -46,5 +46,15 @@ export interface ConsolidatedOptions {
46
46
  environment?: NodeJS.ProcessEnv;
47
47
  diagnostics?: readonly HomeNeedsYouTarget[];
48
48
  }
49
+ /**
50
+ * OBS-1178: the store's incremental fold reads every task-approved row as a release. A refused or unsound
51
+ * decision released nothing, so whenever the rows the store has read hold one — whatever state the raw
52
+ * fold shows now, since a later run-end restores a park but not the gate evidence a rejected recheck
53
+ * cleared — the operator snapshot is refolded over the decided rows, each keeping its physical line and
54
+ * the store's evidence identity.
55
+ * ponytail: a whole-journal read per observed journal change (parsed only when it holds an approval);
56
+ * move the decision fold into OperatorStateFold if that ever measures slow.
57
+ */
58
+ export declare function decidedLiveStore(store: LiveStore): LiveStore;
49
59
  /** The live entry mounts the dependency-owned leaves, using only C2 snapshots/history/page. */
50
60
  export declare function runConsolidatedCockpit(options: ConsolidatedOptions): Promise<void>;
@@ -1,5 +1,5 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
- import { existsSync, renameSync, statSync, unlinkSync, writeFileSync } from "node:fs";
2
+ import { existsSync, readFileSync, renameSync, statSync, unlinkSync, writeFileSync } from "node:fs";
3
3
  import { dirname, resolve } from "node:path";
4
4
  import { renderMarkdownRecord } from "../../cli/commands/report.js";
5
5
  import { loadConfig } from "../../config/config.js";
@@ -20,9 +20,10 @@ import { createPointerReportReader, POINTER_TRACKING_ON, POINTER_TRACKING_OFF }
20
20
  import { deriveRunDecisions, previewDecision, executeDecision, withDecisionPreview, withDecisionReceipt, decisionConfirmLines } from "./decision-actions.js";
21
21
  import { approvalRunOwner } from "../../cli/commands/approve.js";
22
22
  import { GLYPHS } from "../../brand.js";
23
- import { Journal } from "../../run/journal.js";
23
+ import { effectiveEvents, Journal, parseJournalText, physicalLine } from "../../run/journal.js";
24
24
  import { observeNamedRun, WATCH_OWNER_ENV } from "../../run/supervision.js";
25
25
  import { formatOwnedName } from "../../drivers/types.js";
26
+ import { readOperatorState } from "../../run/operator-state.js";
26
27
  import { cellWidth, wrapCells } from "./width.js";
27
28
  import { resolveShellColourMode } from "./theme.js";
28
29
  let timelineUsers = 0;
@@ -58,6 +59,52 @@ export function borrowRuntimeTimeline() {
58
59
  }
59
60
  export const SHELL_TERMINAL_ENTER = "\x1b[?1049h\x1b[22;0t\x1b]0;tickmarkr cockpit\x07" + POINTER_TRACKING_ON;
60
61
  export const SHELL_TERMINAL_RESTORE = POINTER_TRACKING_OFF + "\x1b[23;0t\x1b[?25h\x1b[?1049l";
62
+ /**
63
+ * OBS-1178: the store's incremental fold reads every task-approved row as a release. A refused or unsound
64
+ * decision released nothing, so whenever the rows the store has read hold one — whatever state the raw
65
+ * fold shows now, since a later run-end restores a park but not the gate evidence a rejected recheck
66
+ * cleared — the operator snapshot is refolded over the decided rows, each keeping its physical line and
67
+ * the store's evidence identity.
68
+ * ponytail: a whole-journal read per observed journal change (parsed only when it holds an approval);
69
+ * move the decision fold into OperatorStateFold if that ever measures slow.
70
+ */
71
+ export function decidedLiveStore(store) {
72
+ let key;
73
+ let operator;
74
+ const decided = (snap) => {
75
+ const { journal, graph } = snap;
76
+ const observed = `${journal.source}:${journal.generation}:${journal.offset}:${graph.identity}:${graph.status}`;
77
+ if (observed !== key) {
78
+ key = observed;
79
+ operator = undefined;
80
+ let bytes;
81
+ try {
82
+ bytes = readFileSync(journal.source).subarray(0, journal.offset);
83
+ }
84
+ catch {
85
+ return snap;
86
+ }
87
+ if (bytes.includes("task-approved")) {
88
+ const rows = parseJournalText(bytes.subarray(0, bytes.lastIndexOf(10) + 1).toString("utf8"))
89
+ .filter(e => e !== null && typeof e === "object" && typeof e.event === "string" && typeof e.data === "object" && e.data !== null);
90
+ const kept = effectiveEvents(rows);
91
+ if (kept.length !== rows.length) {
92
+ const events = kept.map((event, i) => {
93
+ const line = physicalLine(kept, i);
94
+ return { source: journal.source, line, id: `${journal.source}#L${line}`, generation: journal.generation, event };
95
+ });
96
+ operator = readOperatorState({ events, graph: graph.status === "readable" ? graph.value : undefined,
97
+ readable: journal.status === "readable" && journal.backlogBytes === 0 });
98
+ }
99
+ }
100
+ }
101
+ if (!operator)
102
+ return snap;
103
+ const { sequence, observedAt, graphAvailability } = snap.operator;
104
+ return { ...snap, operator: { ...operator, sequence, observedAt, graphAvailability } };
105
+ };
106
+ return { ...store, snapshot: () => decided(store.snapshot()), refresh: () => decided(store.refresh()) };
107
+ }
61
108
  /** Preserve a policy-disabled gate from C2 even when C5's generic event
62
109
  * mapper calls an unpassed result unknown. Lead with the gate name so a long
63
110
  * task identity cannot clip the label that explains the disabled glyph. */
@@ -97,7 +144,7 @@ export async function runConsolidatedCockpit(options) {
97
144
  output.write(SHELL_TERMINAL_ENTER);
98
145
  }
99
146
  store = createLiveStore({ cwd, runId, now: options.now });
100
- const source = store;
147
+ const source = decidedLiveStore(store);
101
148
  if (options.observeRun !== false)
102
149
  observation = observeNamedRun(cwd, runId, options.environment);
103
150
  // BD-1 (RULING-231-19 §2): the daemon-placed board carries the watch owner token and mounts
@@ -34,6 +34,9 @@ export declare function deriveRunViewRows(data: RunCockpitData, viewId: RunViewI
34
34
  * task row — identity, phase, build evidence, blocker, next action — each unrecorded field saying so,
35
35
  * each recorded one citing the journal line it was read from, and the OBS-1048 stall marker when the
36
36
  * newest attempt's harvest is suspect. No attempt label is printed here: the ruler is the row's own.
37
+ * A task with no recorded dispatch folds identity, phase, and build into one missing-evidence clause
38
+ * (OBS-1104). A recorded blocker or next action — a pre-dispatch park, a dependency wait — stays,
39
+ * cited to the journal row that produced it.
37
40
  */
38
41
  export declare function taskProjectionText(row: TaskRow, journal?: readonly JournalRow[]): string;
39
42
  /**