tickmarkr 2.1.9 → 2.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/adapters/model-lints.js +9 -0
- package/dist/adapters/pi.d.ts +1 -0
- package/dist/adapters/pi.js +15 -1
- package/dist/adapters/prompt.js +11 -3
- package/dist/adapters/registry.js +13 -4
- package/dist/adapters/types.d.ts +3 -0
- package/dist/adapters/types.js +1 -0
- package/dist/cli/commands/approve.d.ts +20 -0
- package/dist/cli/commands/approve.js +127 -35
- package/dist/cli/commands/doctor.d.ts +1 -1
- package/dist/cli/commands/doctor.js +14 -4
- package/dist/cli/commands/plan.js +4 -1
- package/dist/cli/commands/verify.js +2 -0
- package/dist/cli/commands/version.d.ts +2 -1
- package/dist/cli/commands/version.js +25 -4
- package/dist/compile/collateral.js +149 -4
- package/dist/compile/native.js +2 -0
- package/dist/drivers/index.d.ts +1 -0
- package/dist/drivers/index.js +25 -10
- package/dist/drivers/orca.d.ts +19 -0
- package/dist/drivers/orca.js +45 -9
- package/dist/gates/baseline.d.ts +2 -0
- package/dist/gates/baseline.js +49 -14
- package/dist/gates/llm.d.ts +1 -1
- package/dist/gates/llm.js +41 -26
- package/dist/gates/review.js +2 -0
- package/dist/gates/run-gates.d.ts +6 -0
- package/dist/gates/run-gates.js +9 -1
- package/dist/run/consult.d.ts +7 -0
- package/dist/run/consult.js +16 -2
- package/dist/run/daemon.d.ts +12 -6
- package/dist/run/daemon.js +306 -24
- package/dist/run/journal.d.ts +16 -0
- package/dist/run/journal.js +51 -6
- package/dist/tui/cockpit/setup-cockpit.d.ts +8 -10
- package/dist/tui/cockpit/setup-cockpit.js +92 -53
- package/package.json +1 -1
- package/skills/tickmarkr-loop/SKILL.md +1 -0
- package/skills/tickmarkr-overseer/SKILL.md +17 -0
- package/skills/tickmarkr-overseer/scripts/watch-context.sh +2 -2
package/dist/run/journal.js
CHANGED
|
@@ -148,6 +148,40 @@ export function upheldFeedbackByTask(events) {
|
|
|
148
148
|
}
|
|
149
149
|
return upheld;
|
|
150
150
|
}
|
|
151
|
+
const CONSULT_ACTIONS = ["retry", "reroute", "decompose", "human"];
|
|
152
|
+
const consultAction = (action) => typeof action === "string" && CONSULT_ACTIONS.includes(action)
|
|
153
|
+
? action
|
|
154
|
+
: undefined;
|
|
155
|
+
/**
|
|
156
|
+
* The newest structured consult prescription still open for this task. A consult park is only a
|
|
157
|
+
* pause: graph-changed revival can dispatch a fresh worker without a live `applyVerdict()` frame, so
|
|
158
|
+
* prompt-build re-derives the worker-facing bullets from the journal. Notes are deliberately excluded:
|
|
159
|
+
* they are audit prose, and OBS-37a says workers get only structured reason/guidance rendered by
|
|
160
|
+
* `renderRetryGuidance()`.
|
|
161
|
+
*/
|
|
162
|
+
export function outstandingConsultGuidance(events, taskId) {
|
|
163
|
+
let carry;
|
|
164
|
+
for (const e of events) {
|
|
165
|
+
if (e.taskId !== taskId)
|
|
166
|
+
continue;
|
|
167
|
+
if (e.event === "consult-verdict") {
|
|
168
|
+
const action = consultAction(e.data.action);
|
|
169
|
+
const reason = typeof e.data.reason === "string" && e.data.reason.trim() ? e.data.reason : undefined;
|
|
170
|
+
const guidance = typeof e.data.guidance === "string" && e.data.guidance.trim() ? e.data.guidance : undefined;
|
|
171
|
+
carry = action && (reason || guidance)
|
|
172
|
+
? {
|
|
173
|
+
action,
|
|
174
|
+
...(reason ? { reason } : {}),
|
|
175
|
+
...(guidance ? { guidance } : {}),
|
|
176
|
+
}
|
|
177
|
+
: undefined;
|
|
178
|
+
}
|
|
179
|
+
else if (e.event === "task-done" || e.event === "merge") {
|
|
180
|
+
carry = undefined;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
return carry;
|
|
184
|
+
}
|
|
151
185
|
// Reserved for a finding whose OWN evidence names no path. Reporting a blank path a reader would take
|
|
152
186
|
// for a resolved one is the silent-lie shape the gates exist to refuse, so the field says so outright.
|
|
153
187
|
export const UNIDENTIFIED = "<unidentified>";
|
|
@@ -1360,16 +1394,27 @@ export class Journal {
|
|
|
1360
1394
|
}
|
|
1361
1395
|
return m;
|
|
1362
1396
|
}
|
|
1363
|
-
// OBS-130: gate satisfaction is authority, not an inferred daemon state. Only an explicit
|
|
1364
|
-
// task-approved event with the typed release marker and a known gate enters this fold.
|
|
1365
|
-
//
|
|
1397
|
+
// OBS-130/OBS-571: gate satisfaction is authority, not an inferred daemon state. Only an explicit
|
|
1398
|
+
// task-approved event with the typed release marker and a known gate enters this fold. Authority is
|
|
1399
|
+
// scoped to that approval: any later approval supersedes it, and the worktree-recreation row written
|
|
1400
|
+
// before the approved gate suffix runs consumes it at enactment. A daemon-made gate-satisfied event,
|
|
1401
|
+
// a prior pass/fail result, malformed data, or another task's approval is inert.
|
|
1366
1402
|
replaySatisfiedGates() {
|
|
1367
1403
|
const satisfied = new Map();
|
|
1368
1404
|
for (const e of this.read()) {
|
|
1369
|
-
if (
|
|
1405
|
+
if (!e.taskId)
|
|
1370
1406
|
continue;
|
|
1371
|
-
if (
|
|
1372
|
-
satisfied.
|
|
1407
|
+
if (e.event === "worktree-recreation") {
|
|
1408
|
+
satisfied.delete(e.taskId);
|
|
1409
|
+
continue;
|
|
1410
|
+
}
|
|
1411
|
+
if (e.event === "task-approved") {
|
|
1412
|
+
satisfied.delete(e.taskId);
|
|
1413
|
+
if (e.data.release === GATE_SATISFIED_RELEASE
|
|
1414
|
+
&& typeof e.data.gate === "string"
|
|
1415
|
+
&& GATE_NAMES.includes(e.data.gate)) {
|
|
1416
|
+
satisfied.set(e.taskId, e.data.gate);
|
|
1417
|
+
}
|
|
1373
1418
|
}
|
|
1374
1419
|
}
|
|
1375
1420
|
return satisfied;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type ReactElement } from "react";
|
|
2
|
+
import { type ApprovalDisposition, type ApprovalRunOwner } from "../../cli/commands/approve.js";
|
|
2
3
|
import { Journal } from "../../run/journal.js";
|
|
3
4
|
import { type TickmarkrConfig } from "../../config/config.js";
|
|
4
5
|
import { type ComponentState, type JournalRow } from "./components.js";
|
|
@@ -73,15 +74,8 @@ export type ParkedDecision = {
|
|
|
73
74
|
};
|
|
74
75
|
/** The parks a verb can release — the rows the surface draws live verbs on. */
|
|
75
76
|
export declare function actionableDecisions(decisions: readonly ParkedDecision[]): readonly ParkedDecision[];
|
|
76
|
-
/**
|
|
77
|
-
* The decisions section's rows: one per task the journal's own status replay
|
|
78
|
-
* leaves parked on a human, in the order the journal first named them. The
|
|
79
|
-
* replay is the production one (`Journal.replayStatuses`) so this surface and
|
|
80
|
-
* `tickmarkr status` can never disagree about what is parked.
|
|
81
|
-
*/
|
|
82
77
|
export declare function deriveParkedDecisions(journal: Journal): readonly ParkedDecision[];
|
|
83
|
-
|
|
84
|
-
export type SetupDecisionVerb = "approve" | "uphold";
|
|
78
|
+
export type SetupDecisionVerb = "approve" | "waive" | "uphold" | "recheck";
|
|
85
79
|
/** A decision the operator has named but not yet confirmed. */
|
|
86
80
|
export type SetupDecisionCommand = {
|
|
87
81
|
readonly verb: SetupDecisionVerb;
|
|
@@ -93,6 +87,7 @@ export type SetupDecisionWrite = {
|
|
|
93
87
|
readonly verb: SetupDecisionVerb;
|
|
94
88
|
/** The release marker read back from the recorded task-approved event. */
|
|
95
89
|
readonly release: string | undefined;
|
|
90
|
+
readonly disposition: ApprovalDisposition;
|
|
96
91
|
readonly by: string;
|
|
97
92
|
};
|
|
98
93
|
/**
|
|
@@ -120,6 +115,7 @@ export type SetupDecisionsKeyResult = {
|
|
|
120
115
|
* inert and the keybar does not advertise it — the surface never promises a
|
|
121
116
|
* decision the command must refuse.
|
|
122
117
|
*/
|
|
118
|
+
export declare function setupDecisionVerbs(decision: ParkedDecision): readonly SetupDecisionVerb[];
|
|
123
119
|
export declare function upholdAvailable(decision: ParkedDecision): boolean;
|
|
124
120
|
/**
|
|
125
121
|
* The decisions surface's whole key grammar: ↑↓ move, a approve, u uphold,
|
|
@@ -160,7 +156,7 @@ export declare function recordSetupDecisionOutcome(session: SetupDecisionsSessio
|
|
|
160
156
|
* The confirm inset's lines: the task, the actor, the effect and the file,
|
|
161
157
|
* stated before the single key that can write.
|
|
162
158
|
*/
|
|
163
|
-
export declare function setupDecisionConfirmLines(command: SetupDecisionCommand, decision: ParkedDecision | undefined, actor: string, journalFile: string): readonly string[];
|
|
159
|
+
export declare function setupDecisionConfirmLines(command: SetupDecisionCommand, decision: ParkedDecision | undefined, actor: string, journalFile: string, run: ApprovalRunOwner): readonly string[];
|
|
164
160
|
/**
|
|
165
161
|
* The decisions surface. Every row it draws — decisions, the confirm inset,
|
|
166
162
|
* the pending writes region — is measured and clipped through the width
|
|
@@ -168,12 +164,14 @@ export declare function setupDecisionConfirmLines(command: SetupDecisionCommand,
|
|
|
168
164
|
* neither overflow its row nor be cut mid-cluster. The one unclipped element
|
|
169
165
|
* is `notice`: the production command's own words, verbatim.
|
|
170
166
|
*/
|
|
171
|
-
export declare function SetupDecisionsSurface({ decisions, session, columns, actor, journalFile, }: {
|
|
167
|
+
export declare function SetupDecisionsSurface({ decisions, session, columns, actor, journalFile, run, }: {
|
|
172
168
|
decisions: readonly ParkedDecision[];
|
|
173
169
|
session: SetupDecisionsSession;
|
|
174
170
|
columns: number;
|
|
175
171
|
actor: string;
|
|
176
172
|
journalFile: string;
|
|
173
|
+
/** Who enacts a confirmed write — read from the run lock, never assumed. */
|
|
174
|
+
run: ApprovalRunOwner;
|
|
177
175
|
}): ReactElement;
|
|
178
176
|
/**
|
|
179
177
|
* The setup decisions surface on a live engagement. This is the only cockpit
|
|
@@ -4,8 +4,8 @@ import { userInfo } from "node:os";
|
|
|
4
4
|
import { join } from "node:path";
|
|
5
5
|
import { cloneElement, createElement, useState, } from "react";
|
|
6
6
|
import { parse } from "yaml";
|
|
7
|
-
import { approve } from "../../cli/commands/approve.js";
|
|
8
|
-
import { ATTEMPT_CAP_RELEASE, GATE_SATISFIED_RELEASE, Journal, REVIEW_UPHELD_RELEASE, } from "../../run/journal.js";
|
|
7
|
+
import { approvalDispositionForRelease, approvalEnactment, approvalRunOwner, approve, } from "../../cli/commands/approve.js";
|
|
8
|
+
import { ATTEMPT_CAP_RELEASE, GATE_SATISFIED_RELEASE, Journal, RECHECK_RELEASE, REVIEW_UPHELD_RELEASE, } from "../../run/journal.js";
|
|
9
9
|
import { BANNER, GLYPHS, PLAIN_BANNER, } from "../../brand.js";
|
|
10
10
|
import { DEFAULT_CONFIG, INTEGRITY_FLOOR_SHAPES, TIER_RANK, TickmarkrConfigSchema, unifiedYamlDiff, } from "../../config/config.js";
|
|
11
11
|
import { GATE_NAMES, SHAPES } from "../../graph/schema.js";
|
|
@@ -560,6 +560,20 @@ export function actionableDecisions(decisions) {
|
|
|
560
560
|
* replay is the production one (`Journal.replayStatuses`) so this surface and
|
|
561
561
|
* `tickmarkr status` can never disagree about what is parked.
|
|
562
562
|
*/
|
|
563
|
+
function failedGateForNewestPark(events, taskId, parkedIndex) {
|
|
564
|
+
for (let index = parkedIndex - 1; index >= 0; index -= 1) {
|
|
565
|
+
const event = events[index];
|
|
566
|
+
if (event.taskId !== taskId)
|
|
567
|
+
continue;
|
|
568
|
+
if (event.event === "task-approved" || event.event === "task-human" || event.event === "task-dispatch")
|
|
569
|
+
return undefined;
|
|
570
|
+
if (event.event === "gate-result" && event.data.pass === false
|
|
571
|
+
&& typeof event.data.gate === "string" && GATE_NAMES.includes(event.data.gate)) {
|
|
572
|
+
return event.data.gate;
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
return undefined;
|
|
576
|
+
}
|
|
563
577
|
export function deriveParkedDecisions(journal) {
|
|
564
578
|
const statuses = journal.replayStatuses();
|
|
565
579
|
const events = journal.read();
|
|
@@ -579,12 +593,7 @@ export function deriveParkedDecisions(journal) {
|
|
|
579
593
|
parkedIndex = index;
|
|
580
594
|
}
|
|
581
595
|
const parked = parkedIndex >= 0 ? events[parkedIndex] : undefined;
|
|
582
|
-
|
|
583
|
-
// gate-result before the newest task-human controls what approve records
|
|
584
|
-
// and whether uphold exists at all. Never inferred from the park's prose.
|
|
585
|
-
const failedGate = events.slice(0, Math.max(0, parkedIndex)).reverse().find((event) => event.event === "gate-result" && event.taskId === taskId && event.data.pass === false
|
|
586
|
-
&& typeof event.data.gate === "string"
|
|
587
|
-
&& GATE_NAMES.includes(event.data.gate))?.data.gate;
|
|
596
|
+
const failedGate = failedGateForNewestPark(events, taskId, parkedIndex);
|
|
588
597
|
const kind = typeof parked?.data.kind === "string" ? parked.data.kind : "human-gate";
|
|
589
598
|
const reason = typeof parked?.data.reason === "string" ? parked.data.reason : undefined;
|
|
590
599
|
decisions.push({
|
|
@@ -610,8 +619,17 @@ export function initialSetupDecisionsSession() {
|
|
|
610
619
|
* inert and the keybar does not advertise it — the surface never promises a
|
|
611
620
|
* decision the command must refuse.
|
|
612
621
|
*/
|
|
622
|
+
export function setupDecisionVerbs(decision) {
|
|
623
|
+
if (decision.tombstone)
|
|
624
|
+
return [];
|
|
625
|
+
if (decision.kind !== "gate-fail")
|
|
626
|
+
return ["approve"];
|
|
627
|
+
if (decision.failedGate === undefined)
|
|
628
|
+
return [];
|
|
629
|
+
return decision.failedGate === "review" ? ["waive", "uphold", "recheck"] : ["waive", "recheck"];
|
|
630
|
+
}
|
|
613
631
|
export function upholdAvailable(decision) {
|
|
614
|
-
return decision.
|
|
632
|
+
return setupDecisionVerbs(decision).includes("uphold");
|
|
615
633
|
}
|
|
616
634
|
/**
|
|
617
635
|
* The decisions surface's whole key grammar: ↑↓ move, a approve, u uphold,
|
|
@@ -650,25 +668,30 @@ export function applySetupDecisionsKey(session, event, parked) {
|
|
|
650
668
|
},
|
|
651
669
|
};
|
|
652
670
|
}
|
|
653
|
-
|
|
671
|
+
const verb = { a: "approve", w: "waive", u: "uphold", r: "recheck" }[event.input ?? ""];
|
|
672
|
+
if (verb !== undefined) {
|
|
654
673
|
const decision = decisions[selection];
|
|
655
|
-
if (decision === undefined)
|
|
656
|
-
return { session };
|
|
657
|
-
if (event.input === "u" && !upholdAvailable(decision))
|
|
674
|
+
if (decision === undefined || !setupDecisionVerbs(decision).includes(verb))
|
|
658
675
|
return { session };
|
|
659
676
|
return {
|
|
660
677
|
session: {
|
|
661
678
|
...session,
|
|
662
679
|
selection,
|
|
663
|
-
confirming: {
|
|
664
|
-
verb: event.input === "a" ? "approve" : "uphold",
|
|
665
|
-
taskId: decision.taskId,
|
|
666
|
-
},
|
|
680
|
+
confirming: { verb, taskId: decision.taskId },
|
|
667
681
|
},
|
|
668
682
|
};
|
|
669
683
|
}
|
|
670
684
|
return { session };
|
|
671
685
|
}
|
|
686
|
+
const decisionFlag = (verb) => {
|
|
687
|
+
if (verb === "waive")
|
|
688
|
+
return ["--waive"];
|
|
689
|
+
if (verb === "uphold")
|
|
690
|
+
return ["--uphold"];
|
|
691
|
+
if (verb === "recheck")
|
|
692
|
+
return ["--recheck"];
|
|
693
|
+
return [];
|
|
694
|
+
};
|
|
672
695
|
/**
|
|
673
696
|
* THE write. It calls the production `approve` command — validation, fail-closed
|
|
674
697
|
* refusals and the journal append are inherited, never re-implemented — and then
|
|
@@ -680,7 +703,7 @@ export async function executeSetupDecision(command, { cwd, runId, by }) {
|
|
|
680
703
|
const message = await approve([
|
|
681
704
|
runId,
|
|
682
705
|
command.taskId,
|
|
683
|
-
...(command.verb
|
|
706
|
+
...decisionFlag(command.verb),
|
|
684
707
|
"--by",
|
|
685
708
|
by,
|
|
686
709
|
], cwd);
|
|
@@ -693,11 +716,12 @@ export async function executeSetupDecision(command, { cwd, runId, by }) {
|
|
|
693
716
|
break;
|
|
694
717
|
}
|
|
695
718
|
}
|
|
719
|
+
const disposition = approvalDispositionForRelease(release);
|
|
696
720
|
return {
|
|
697
721
|
ok: true,
|
|
698
722
|
command,
|
|
699
723
|
message,
|
|
700
|
-
write: { taskId: command.taskId, verb: command.verb, release, by },
|
|
724
|
+
write: { taskId: command.taskId, verb: command.verb, release, disposition, by },
|
|
701
725
|
};
|
|
702
726
|
}
|
|
703
727
|
catch (error) {
|
|
@@ -726,52 +750,58 @@ function decisionRowText(decision) {
|
|
|
726
750
|
* append — the release markers are the command's own constants, so this text
|
|
727
751
|
* cannot drift from the write. An effect the operator cannot read here is an
|
|
728
752
|
* effect this surface does not have.
|
|
753
|
+
*
|
|
754
|
+
* WHO enacts it is the command's own sentence (`approvalEnactment`), read from
|
|
755
|
+
* the same run-owner fact: predicting "the next resume" over a LIVE run was
|
|
756
|
+
* true only while the daemon read approvals once at startup. It now sweeps them
|
|
757
|
+
* at every task boundary, so that prediction would send the operator to start a
|
|
758
|
+
* second run in this repository — forbidden — for work already scheduled.
|
|
729
759
|
*/
|
|
730
|
-
function decisionEffectLines(command, decision) {
|
|
731
|
-
if (command.verb === "
|
|
760
|
+
function decisionEffectLines(command, decision, run) {
|
|
761
|
+
if (command.verb === "waive") {
|
|
762
|
+
const gate = decision?.failedGate;
|
|
763
|
+
return gate === undefined ? ["effect none — waive refuses without a failed gate"] : [
|
|
764
|
+
`effect disposition waive-gate; appends release ${GATE_SATISFIED_RELEASE} for gate ${gate}`,
|
|
765
|
+
`it marks gate ${gate} satisfied; it does NOT mark the task done`,
|
|
766
|
+
approvalEnactment("waive-gate", run),
|
|
767
|
+
];
|
|
768
|
+
}
|
|
769
|
+
if (command.verb === "recheck") {
|
|
732
770
|
return [
|
|
733
|
-
`effect
|
|
734
|
-
|
|
735
|
-
"
|
|
771
|
+
`effect disposition re-dispatch; appends release ${RECHECK_RELEASE}`,
|
|
772
|
+
"it marks no gate satisfied",
|
|
773
|
+
approvalEnactment("re-dispatch", run),
|
|
736
774
|
];
|
|
737
775
|
}
|
|
738
|
-
if (
|
|
739
|
-
if (decision.failedGate === undefined) {
|
|
740
|
-
// The command's own refusal, predicted: no write is promised here.
|
|
741
|
-
return [
|
|
742
|
-
"effect none — the command refuses: parked on gate-fail"
|
|
743
|
-
+ " but no failed gate result is recorded",
|
|
744
|
-
];
|
|
745
|
-
}
|
|
746
|
-
const gate = decision.failedGate;
|
|
776
|
+
if (command.verb === "uphold") {
|
|
747
777
|
return [
|
|
748
|
-
`effect
|
|
749
|
-
|
|
750
|
-
`it marks gate ${gate} satisfied; it does NOT mark the task done`,
|
|
778
|
+
`effect disposition fund-fixed-attempt; appends release ${REVIEW_UPHELD_RELEASE}`,
|
|
779
|
+
approvalEnactment("fund-fixed-attempt", run),
|
|
751
780
|
];
|
|
752
781
|
}
|
|
782
|
+
if (decision?.kind === "gate-fail") {
|
|
783
|
+
return ["effect none — approve is not offered on gate-fail parks; choose waive, recheck or uphold"];
|
|
784
|
+
}
|
|
753
785
|
if (decision?.kind === ATTEMPT_CAP_RELEASE) {
|
|
754
786
|
return [
|
|
755
|
-
`effect
|
|
756
|
-
|
|
757
|
-
"it does NOT mark the task done or pass any gate",
|
|
787
|
+
`effect disposition fresh-budget; appends release ${ATTEMPT_CAP_RELEASE}`,
|
|
788
|
+
approvalEnactment("fresh-budget", run),
|
|
758
789
|
];
|
|
759
790
|
}
|
|
760
791
|
return [
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
"it does NOT mark the task done or pass any gate",
|
|
792
|
+
`effect disposition dispatch; appends one task-approved event with no release`,
|
|
793
|
+
approvalEnactment("dispatch", run),
|
|
764
794
|
];
|
|
765
795
|
}
|
|
766
796
|
/**
|
|
767
797
|
* The confirm inset's lines: the task, the actor, the effect and the file,
|
|
768
798
|
* stated before the single key that can write.
|
|
769
799
|
*/
|
|
770
|
-
export function setupDecisionConfirmLines(command, decision, actor, journalFile) {
|
|
800
|
+
export function setupDecisionConfirmLines(command, decision, actor, journalFile, run) {
|
|
771
801
|
return [
|
|
772
802
|
`task ${command.taskId}${decision === undefined ? "" : ` · ${decision.kind} · attempt ${decision.attempts}`}`,
|
|
773
803
|
`actor ${actor}`,
|
|
774
|
-
...decisionEffectLines(command, decision),
|
|
804
|
+
...decisionEffectLines(command, decision, run),
|
|
775
805
|
`file ${journalFile} · append only`,
|
|
776
806
|
`y ${command.verb} · n cancel`,
|
|
777
807
|
];
|
|
@@ -783,8 +813,13 @@ function setupDecisionsKeybar(session, decisions) {
|
|
|
783
813
|
if (decisions.length === 0)
|
|
784
814
|
return "q Quit";
|
|
785
815
|
const selected = Math.min(session.selection, decisions.length - 1);
|
|
786
|
-
const
|
|
787
|
-
|
|
816
|
+
const verbs = setupDecisionVerbs(decisions[selected]).map((verb) => ({
|
|
817
|
+
approve: "a Approve",
|
|
818
|
+
waive: "w Waive",
|
|
819
|
+
uphold: "u Uphold",
|
|
820
|
+
recheck: "r Recheck",
|
|
821
|
+
})[verb]).join(" · ");
|
|
822
|
+
return `↑↓ Move · ${verbs} · q Quit`;
|
|
788
823
|
}
|
|
789
824
|
/**
|
|
790
825
|
* What the tab is, drawn on the tab it names. The operator's v1.83 UAT read
|
|
@@ -793,7 +828,7 @@ function setupDecisionsKeybar(session, decisions) {
|
|
|
793
828
|
* keybar hint also draw.
|
|
794
829
|
*/
|
|
795
830
|
const DECISIONS_TAB_LINE = `tab ${DECISIONS_TAB_HINT} · journal-parked human decisions`
|
|
796
|
-
+ " · approve
|
|
831
|
+
+ " · approve waive uphold and recheck are its only writes";
|
|
797
832
|
/**
|
|
798
833
|
* What the section says when no park needs the operator. It names its own
|
|
799
834
|
* trigger and both verbs, so an operator who arrives at a quiet surface learns
|
|
@@ -801,8 +836,8 @@ const DECISIONS_TAB_LINE = `tab ${DECISIONS_TAB_HINT} · journal-parked human de
|
|
|
801
836
|
*/
|
|
802
837
|
const DECISIONS_EMPTY_STATE = [
|
|
803
838
|
"nothing needs you now — no task is parked on a human gate",
|
|
804
|
-
"a task that parks on one appears here with its
|
|
805
|
-
+ " approve
|
|
839
|
+
"a task that parks on one appears here with its decisions —"
|
|
840
|
+
+ " approve dispatches, waive accepts a gate, uphold funds a fix, recheck re-runs",
|
|
806
841
|
];
|
|
807
842
|
/**
|
|
808
843
|
* The decisions surface. Every row it draws — decisions, the confirm inset,
|
|
@@ -811,15 +846,16 @@ const DECISIONS_EMPTY_STATE = [
|
|
|
811
846
|
* neither overflow its row nor be cut mid-cluster. The one unclipped element
|
|
812
847
|
* is `notice`: the production command's own words, verbatim.
|
|
813
848
|
*/
|
|
814
|
-
export function SetupDecisionsSurface({ decisions, session, columns, actor, journalFile, }) {
|
|
849
|
+
export function SetupDecisionsSurface({ decisions, session, columns, actor, journalFile, run, }) {
|
|
815
850
|
const inner = Math.max(1, Math.floor(columns) - 4);
|
|
816
851
|
const actionable = actionableDecisions(decisions);
|
|
817
852
|
const tombstones = decisions.filter((decision) => decision.tombstone);
|
|
818
853
|
const selected = Math.min(session.selection, Math.max(0, actionable.length - 1));
|
|
819
854
|
const confirming = session.confirming;
|
|
820
|
-
return (_jsxs(Box, { flexDirection: "column", width: columns, children: [_jsxs(Panel, { title: `${DECISIONS_TAB_TITLE} · ${decisions.length} parked`, focused: true, children: [_jsx(BodyText, { emphasis: "dim", children: fitCells(DECISIONS_TAB_LINE, inner) }), actionable.map((decision, index) => (_jsx(BodyText, { emphasis: index === selected ? "strong" : "normal", children: fitCells(`${index === selected ? `${GLYPHS.pointer} ` : " "}${decisionRowText(decision)}`, inner) }, decision.taskId))), actionable.length === 0 && DECISIONS_EMPTY_STATE.map((line) => (_jsx(BodyText, { children: fitCells(line, inner) }, line))), tombstones.map((decision) => (_jsx(BodyText, { emphasis: "dim", children: fitCells(` read-only · permanent by design · ${decisionRowText(decision)}`, inner) }, decision.taskId)))] }), confirming !== null && (_jsx(Panel, { title: `CONFIRM ${confirming.verb.toUpperCase()}`, focused: true, children: setupDecisionConfirmLines(confirming, decisions.find((decision) => decision.taskId === confirming.taskId), actor, journalFile).map((line) => (_jsx(BodyText, { children: fitCells(line, inner) }, line))) })), session.notice !== null && _jsx(BodyText, { children: session.notice }), _jsx(Panel, { title: "PENDING WRITES", children: session.writes.length === 0
|
|
855
|
+
return (_jsxs(Box, { flexDirection: "column", width: columns, children: [_jsxs(Panel, { title: `${DECISIONS_TAB_TITLE} · ${decisions.length} parked`, focused: true, children: [_jsx(BodyText, { emphasis: "dim", children: fitCells(DECISIONS_TAB_LINE, inner) }), actionable.map((decision, index) => (_jsx(BodyText, { emphasis: index === selected ? "strong" : "normal", children: fitCells(`${index === selected ? `${GLYPHS.pointer} ` : " "}${decisionRowText(decision)}`, inner) }, decision.taskId))), actionable.length === 0 && DECISIONS_EMPTY_STATE.map((line) => (_jsx(BodyText, { children: fitCells(line, inner) }, line))), tombstones.map((decision) => (_jsx(BodyText, { emphasis: "dim", children: fitCells(` read-only · permanent by design · ${decisionRowText(decision)}`, inner) }, decision.taskId)))] }), confirming !== null && (_jsx(Panel, { title: `CONFIRM ${confirming.verb.toUpperCase()}`, focused: true, children: setupDecisionConfirmLines(confirming, decisions.find((decision) => decision.taskId === confirming.taskId), actor, journalFile, run).map((line) => (_jsx(BodyText, { children: fitCells(line, inner) }, line))) })), session.notice !== null && _jsx(BodyText, { children: session.notice }), _jsx(Panel, { title: "PENDING WRITES", children: session.writes.length === 0
|
|
821
856
|
? _jsx(BodyText, { children: fitCells("nothing written this session", inner) })
|
|
822
|
-
: session.writes.map((write) => (_jsx(BodyText, { children: fitCells(`${write.verb} ${write.taskId} ·
|
|
857
|
+
: session.writes.map((write) => (_jsx(BodyText, { children: fitCells(`${write.verb} ${write.taskId} · disposition ${write.disposition}`
|
|
858
|
+
+ ` · release ${write.release ?? "none"} · by ${write.by}`, inner) }, `${write.verb}:${write.taskId}`))) }), _jsx(BodyText, { emphasis: "dim", children: fitCells(setupDecisionsKeybar(session, actionable), Math.max(1, Math.floor(columns))) })] }));
|
|
823
859
|
}
|
|
824
860
|
function SetupDecisionsApp({ cwd, runId, actor, }) {
|
|
825
861
|
const { exit } = useApp();
|
|
@@ -846,7 +882,10 @@ function SetupDecisionsApp({ cwd, runId, actor, }) {
|
|
|
846
882
|
});
|
|
847
883
|
}
|
|
848
884
|
});
|
|
849
|
-
return (_jsx(SetupDecisionsSurface, { decisions: decisions, session: session, columns: stdout.columns ?? 80, actor: actor, journalFile: join(stateDirName(cwd), "runs", runId, "journal.jsonl")
|
|
885
|
+
return (_jsx(SetupDecisionsSurface, { decisions: decisions, session: session, columns: stdout.columns ?? 80, actor: actor, journalFile: join(stateDirName(cwd), "runs", runId, "journal.jsonl"),
|
|
886
|
+
// read per render, not once at mount: a daemon can start or end while the
|
|
887
|
+
// operator is deciding, and the inset must not promise the wrong enactor.
|
|
888
|
+
run: approvalRunOwner(cwd, runId) }));
|
|
850
889
|
}
|
|
851
890
|
/**
|
|
852
891
|
* The setup decisions surface on a live engagement. This is the only cockpit
|
package/package.json
CHANGED
|
@@ -29,6 +29,7 @@ Outside a multi-agent terminal environment, run the loop directly.
|
|
|
29
29
|
- Do not edit the compiled graph to force an outcome; fix the source spec and compile again.
|
|
30
30
|
- Gates verify commits, diffs, acceptance criteria, and reviews independently. Never trust a worker's claim that work is complete.
|
|
31
31
|
- Treat missing or unparseable machine results and verdicts as failures. Do not release, resume, or merge around failed gates.
|
|
32
|
+
- A task changing what the daemon DOES must own every surface that TELLS the operator what the daemon does.
|
|
32
33
|
|
|
33
34
|
## Act by default
|
|
34
35
|
|
|
@@ -487,6 +487,12 @@ they are left implicit:
|
|
|
487
487
|
were authed on 2026-08-17 while every seat ran claude). Priority when independence is scarce:
|
|
488
488
|
**verifier > checker > planner > executors** — the independent seat goes cross-vendor
|
|
489
489
|
(`herdr agent start … --kind codex`), ruled at dispatch, never debated under time pressure.
|
|
490
|
+
**A codex seat inside a git WORKTREE cannot commit and cannot write outside the worktree** (OBS-824, measured
|
|
491
|
+
twice on 2026-09-01): its sandbox pins writes to the worktree's own path, and a worktree's `.git` is a FILE pointing
|
|
492
|
+
at the main repository's object store, so every `git commit` from inside it is refused. Brief such a seat to leave its
|
|
493
|
+
report INSIDE the worktree and to commit nothing — the overseer commits from the main checkout — or give the work to a
|
|
494
|
+
claude seat, or to a throwaway CLONE (a real `.git` directory). A brief that tells a codex-in-worktree seat to commit
|
|
495
|
+
buys a stall, not a commit.
|
|
490
496
|
4. **Gate every exec lane with the shipped battery, not hand-rolled greps.**
|
|
491
497
|
`tickmarkr verify --base <ref> --criteria <file>` is the standalone form of the engine's own gates —
|
|
492
498
|
build/test/lint diffed against a recorded baseline, evidence, scope, plus the semantic judges — one
|
|
@@ -749,6 +755,10 @@ once** — an overseer ran nine hours at 86% unable to read its own number. Arm
|
|
|
749
755
|
Herdr's `agent_status`, which is unreliable for vendors whose screen detection is skipped (kimi) — for
|
|
750
756
|
those, the spawn-time auto-approve flag is the control, not this watcher.
|
|
751
757
|
|
|
758
|
+
For `consult`, `surgeon`, and `reviewer` seats, the CONTEXT watcher is wake-only: arm it with
|
|
759
|
+
`TKR_AUTO_CLEAR=0 .claude/skills/tickmarkr-overseer/scripts/watch-context.sh <consult|surgeon|reviewer>
|
|
760
|
+
<agent|pane> 50 50 <handoff-file>`. Only `orchestrator` and `overseer` seats may use auto-clear.
|
|
761
|
+
|
|
752
762
|
```bash
|
|
753
763
|
.claude/skills/tickmarkr-overseer/scripts/watch-pending-input.sh <agent|pane> [poll-s] [cap-s] [confirm-polls]
|
|
754
764
|
```
|
|
@@ -947,6 +957,13 @@ orchestrator turn boundary.
|
|
|
947
957
|
leave clean state after a version is shipped"***. Every line below is a defect that actually happened
|
|
948
958
|
on the release that produced this rule.
|
|
949
959
|
|
|
960
|
+
- **RUN BOTH RELEASE PROOFS BEFORE STANDING DOWN.** The first is the exported-tree suite, which
|
|
961
|
+
maintainers run from the private repo with its `verify:export` script — that tooling is deliberately
|
|
962
|
+
not part of this package, so the command does not exist in a public checkout; it must install,
|
|
963
|
+
build, and run the suite in the exported tree; then
|
|
964
|
+
`TICKMARKR_E2E=1 npx vitest run tests/e2e/orca-smoke.e2e.test.ts` must exercise the installed-Orca
|
|
965
|
+
smoke. A skipped smoke is not proof. Record both results before declaring the release complete.
|
|
966
|
+
|
|
950
967
|
- **REWRITE THE MEMORY INDEX FIRST, and read it back.** Minutes after `2.1.1` hit npm, the index line
|
|
951
968
|
a fresh session loads still read *"⛔ 2.1.1 CANNOT ship from run …2011"* — true when written, and by
|
|
952
969
|
then the exact opposite of the truth. **The index is what everyone loads and the body is what nobody
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
# usage: watch-context.sh <role-slug> <agent|pane> <warn-pct> <act-pct> [handoff-file] [poll-s] [cap-s]
|
|
34
34
|
# <role-slug> is ANY seat role — orchestrator, overseer, surgeon, consult — and names the tier
|
|
35
35
|
# `<role>-context`. It is deliberately NOT a closed set: see OBS-730 at the guard below.
|
|
36
|
-
# TKR_AUTO_CLEAR=1 at act-pct WITH a fresh handoff,
|
|
36
|
+
# TKR_AUTO_CLEAR=1 at act-pct WITH a fresh handoff, auto-clear orchestrator/overseer; other roles wake only.
|
|
37
37
|
# TKR_REBRIEF=<path> the file the re-briefed seat is told to read (defaults to the handoff).
|
|
38
38
|
# TKR_HANDOFF_MAX_AGE_S how fresh "fresh" is (default 900).
|
|
39
39
|
# TKR_CLEAR_SETTLE_S seconds to let a cleared seat settle before the re-brief (default 6).
|
|
@@ -221,7 +221,7 @@ handoff_fresh() {
|
|
|
221
221
|
act_on() {
|
|
222
222
|
local P="$1"
|
|
223
223
|
if handoff_fresh; then
|
|
224
|
-
if [ "${TKR_AUTO_CLEAR:-0}" = "1" ]; then
|
|
224
|
+
if [ "${TKR_AUTO_CLEAR:-0}" = "1" ] && { [ "$ROLE" = "orchestrator" ] || [ "$ROLE" = "overseer" ]; }; then
|
|
225
225
|
herdr agent prompt "$TARGET" "/clear" >/dev/null 2>&1
|
|
226
226
|
sleep "$SETTLE"
|
|
227
227
|
herdr agent prompt "$TARGET" "Read ${REBRIEF} and continue exactly where it says. Your context was cleared at ${P}% against that handoff; it is current as of $(date '+%H:%M'). Do not reconstruct from memory — everything you need is on disk." >/dev/null 2>&1
|