tickmarkr 2.1.9 → 2.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/commands/approve.d.ts +24 -0
- package/dist/cli/commands/approve.js +121 -34
- package/dist/compile/collateral.js +149 -4
- package/dist/run/daemon.d.ts +10 -6
- package/dist/run/daemon.js +275 -22
- 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-overseer/SKILL.md +6 -0
|
@@ -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
|
@@ -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
|