tickmarkr 2.5.5 → 2.5.7

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 (69) hide show
  1. package/README.md +3 -1
  2. package/dist/adapters/prompt.js +21 -1
  3. package/dist/cli/commands/approve.js +69 -8
  4. package/dist/cli/commands/doctor.d.ts +10 -0
  5. package/dist/cli/commands/fleet.js +4 -0
  6. package/dist/cli/commands/plan.js +20 -4
  7. package/dist/cli/commands/status.js +95 -34
  8. package/dist/cli/commands/verify.js +108 -85
  9. package/dist/config/config.d.ts +11 -0
  10. package/dist/config/config.js +21 -12
  11. package/dist/config/fleet-overlay.js +55 -17
  12. package/dist/drivers/index.js +2 -1
  13. package/dist/drivers/orca.d.ts +21 -1
  14. package/dist/drivers/orca.js +209 -27
  15. package/dist/gates/baseline.d.ts +21 -5
  16. package/dist/gates/baseline.js +67 -17
  17. package/dist/gates/cache.d.ts +14 -13
  18. package/dist/gates/cache.js +17 -5
  19. package/dist/gates/llm.d.ts +3 -0
  20. package/dist/gates/llm.js +11 -0
  21. package/dist/gates/review.d.ts +28 -3
  22. package/dist/gates/review.js +118 -15
  23. package/dist/gates/run-gates.d.ts +10 -2
  24. package/dist/gates/run-gates.js +362 -108
  25. package/dist/gates/test-manifest.d.ts +33 -1
  26. package/dist/gates/test-manifest.js +132 -40
  27. package/dist/gates/test-reporter.js +20 -7
  28. package/dist/graph/graph.d.ts +4 -0
  29. package/dist/graph/graph.js +50 -1
  30. package/dist/run/activity.d.ts +28 -0
  31. package/dist/run/activity.js +194 -0
  32. package/dist/run/consult.js +5 -4
  33. package/dist/run/daemon.d.ts +11 -0
  34. package/dist/run/daemon.js +663 -128
  35. package/dist/run/execution-budget.d.ts +25 -0
  36. package/dist/run/execution-budget.js +142 -0
  37. package/dist/run/git.d.ts +46 -1
  38. package/dist/run/git.js +149 -12
  39. package/dist/run/journal.d.ts +23 -5
  40. package/dist/run/journal.js +98 -25
  41. package/dist/run/lease.d.ts +44 -0
  42. package/dist/run/lease.js +226 -3
  43. package/dist/run/operator-page-summary.d.ts +56 -0
  44. package/dist/run/operator-page-summary.js +68 -0
  45. package/dist/run/operator-summary.d.ts +69 -0
  46. package/dist/run/operator-summary.js +77 -0
  47. package/dist/run/protocol.d.ts +71 -0
  48. package/dist/run/protocol.js +32 -0
  49. package/dist/run/recovery.d.ts +8 -0
  50. package/dist/run/recovery.js +25 -0
  51. package/dist/run/repair-selection.d.ts +12 -0
  52. package/dist/run/repair-selection.js +56 -0
  53. package/dist/run/stall.d.ts +6 -1
  54. package/dist/run/stall.js +60 -3
  55. package/dist/tui/cockpit/board.d.ts +9 -0
  56. package/dist/tui/cockpit/board.js +10 -0
  57. package/dist/tui/cockpit/derive.d.ts +35 -0
  58. package/dist/tui/cockpit/derive.js +152 -10
  59. package/dist/tui/cockpit/evidence-view.d.ts +2 -0
  60. package/dist/tui/cockpit/evidence-view.js +42 -12
  61. package/dist/tui/cockpit/run-cockpit.d.ts +8 -1
  62. package/dist/tui/cockpit/run-cockpit.js +95 -1
  63. package/dist/tui/cockpit/run-view.d.ts +37 -0
  64. package/dist/tui/cockpit/run-view.js +189 -2
  65. package/dist/tui/ink/fleet-app.d.ts +10 -2
  66. package/dist/tui/ink/fleet-app.js +33 -15
  67. package/package.json +2 -2
  68. package/skills/tickmarkr-overseer/SKILL.md +55 -3
  69. package/skills/tickmarkr-overseer/scripts/grade-ci.sh +29 -2
@@ -0,0 +1,69 @@
1
+ /** Labels describe blockers, not journal park kinds or commands. */
2
+ export declare const BLOCKER_KINDS: readonly ["human-decision", "retry", "cooldown", "dependency-wait", "stale-receipt", "missing-capability", "unknown"];
3
+ export type BlockerKind = (typeof BLOCKER_KINDS)[number];
4
+ /**
5
+ * Structural boundary: callers compute park with newestPark and verbs with
6
+ * permittedDecisionVerbs in cli/commands/approve. RunDecision also fits this
7
+ * shape. This module neither imports a surface nor repeats its decision table.
8
+ * Supply only current parks, from the same snapshot as the task statuses.
9
+ */
10
+ export interface OperatorDecisionSnapshot {
11
+ readonly taskId: string;
12
+ readonly park: {
13
+ readonly kind: string | undefined;
14
+ readonly tombstone: boolean;
15
+ readonly reason?: string;
16
+ };
17
+ readonly verbs: readonly string[];
18
+ readonly diagnostic?: string;
19
+ }
20
+ export interface RecordedResponsibility {
21
+ readonly role?: string;
22
+ readonly agent?: string;
23
+ }
24
+ /**
25
+ * Current, already-reduced evidence supplied by the reader. Optional facts stay
26
+ * absent: a wait duration is not a wake timestamp, a channel is not an owner,
27
+ * and elapsed wall time alone cannot establish that a receipt is stale.
28
+ */
29
+ export interface OperatorSummaryTask {
30
+ readonly id: string;
31
+ readonly status: string;
32
+ readonly deps: readonly string[];
33
+ readonly phase?: string;
34
+ readonly lastEvidenceAt?: string;
35
+ readonly responsible?: RecordedResponsibility;
36
+ readonly wait?: {
37
+ readonly kind: "retry" | "cooldown";
38
+ readonly wakeAt?: string;
39
+ readonly diagnostic?: string;
40
+ };
41
+ readonly staleReceipt?: {
42
+ readonly diagnostic: string;
43
+ };
44
+ readonly missingCapability?: {
45
+ readonly capability: string;
46
+ readonly diagnostic?: string;
47
+ };
48
+ }
49
+ export interface OperatorBlocker {
50
+ kind: BlockerKind;
51
+ permittedActions: string[];
52
+ humanInterventionRequired: boolean;
53
+ decisionRequired: boolean;
54
+ diagnostic?: string;
55
+ nextAction: string | null;
56
+ prerequisites?: string[];
57
+ wakeAt?: string;
58
+ capability?: string;
59
+ }
60
+ export interface OperatorTaskSummary {
61
+ taskId: string;
62
+ phase: string | null;
63
+ lastEvidenceAt: string | null;
64
+ /** null explicitly means unknown; never inferred from a park or wait. */
65
+ responsible: RecordedResponsibility | null;
66
+ blocker: OperatorBlocker | null;
67
+ }
68
+ /** Pure projection in current-task order; no clock, host access, or retained state. */
69
+ export declare function projectOperatorSummary(tasks: readonly OperatorSummaryTask[], decisions: readonly OperatorDecisionSnapshot[]): OperatorTaskSummary[];
@@ -0,0 +1,77 @@
1
+ /** Labels describe blockers, not journal park kinds or commands. */
2
+ export const BLOCKER_KINDS = [
3
+ "human-decision", "retry", "cooldown", "dependency-wait", "stale-receipt",
4
+ "missing-capability", "unknown",
5
+ ];
6
+ const SETTLED = new Set(["done", "completed", "merged"]);
7
+ const emptyBlocker = (kind) => ({
8
+ kind, permittedActions: [], humanInterventionRequired: false,
9
+ decisionRequired: false, nextAction: null,
10
+ });
11
+ /** Pure projection in current-task order; no clock, host access, or retained state. */
12
+ export function projectOperatorSummary(tasks, decisions) {
13
+ const statuses = new Map(tasks.map(task => [task.id, task.status]));
14
+ const parks = new Map(decisions.map(decision => [decision.taskId, decision]));
15
+ return tasks.map(task => {
16
+ let blocker = null;
17
+ const decision = task.status === "human" ? parks.get(task.id) : undefined;
18
+ const terminal = SETTLED.has(task.status) || task.status === "failed";
19
+ if (decision) {
20
+ const { park, verbs, diagnostic } = decision;
21
+ blocker = {
22
+ ...emptyBlocker(park.tombstone ? "unknown" : "human-decision"),
23
+ permittedActions: [...verbs],
24
+ humanInterventionRequired: !park.tombstone,
25
+ decisionRequired: verbs.length > 0,
26
+ ...(diagnostic !== undefined || park.reason !== undefined
27
+ ? { diagnostic: diagnostic ?? park.reason } : {}),
28
+ nextAction: verbs.length > 0 ? `Choose a decision: ${verbs.join(", ")}` : null,
29
+ };
30
+ if (!park.tombstone && park.kind === "infra" && task.missingCapability) {
31
+ const { capability, diagnostic: capabilityDiagnostic } = task.missingCapability;
32
+ blocker.kind = "missing-capability";
33
+ blocker.capability = capability;
34
+ blocker.nextAction = `Diagnose host capability: ${capability}`;
35
+ if (diagnostic === undefined && capabilityDiagnostic !== undefined)
36
+ blocker.diagnostic = capabilityDiagnostic;
37
+ }
38
+ }
39
+ else if (task.status === "human") {
40
+ // Missing decision evidence must not make a parked task look automatic.
41
+ blocker = { ...emptyBlocker("unknown"), humanInterventionRequired: true };
42
+ }
43
+ else if (!terminal) {
44
+ const unmet = task.status === "pending" ? task.deps.filter(id => !SETTLED.has(statuses.get(id))) : [];
45
+ if (unmet.length > 0) {
46
+ blocker = {
47
+ ...emptyBlocker("dependency-wait"), prerequisites: unmet,
48
+ nextAction: `Wait for prerequisites: ${unmet.join(", ")}`,
49
+ };
50
+ }
51
+ else if (task.staleReceipt) {
52
+ blocker = {
53
+ ...emptyBlocker("stale-receipt"), diagnostic: task.staleReceipt.diagnostic,
54
+ nextAction: "Inspect the stale receipt and refresh its evidence",
55
+ };
56
+ }
57
+ else if (task.wait) {
58
+ blocker = {
59
+ ...emptyBlocker(task.wait.kind),
60
+ ...(task.wait.diagnostic === undefined ? {} : { diagnostic: task.wait.diagnostic }),
61
+ ...(task.wait.wakeAt === undefined ? {} : { wakeAt: task.wait.wakeAt }),
62
+ };
63
+ }
64
+ else if (task.status === "blocked" || task.status === "unknown") {
65
+ blocker = emptyBlocker("unknown");
66
+ }
67
+ }
68
+ const owner = task.responsible;
69
+ return {
70
+ taskId: task.id, phase: task.phase ?? null, lastEvidenceAt: task.lastEvidenceAt ?? null,
71
+ responsible: owner?.role !== undefined || owner?.agent !== undefined
72
+ ? { ...(owner.role === undefined ? {} : { role: owner.role }),
73
+ ...(owner.agent === undefined ? {} : { agent: owner.agent }) } : null,
74
+ blocker,
75
+ };
76
+ });
77
+ }
@@ -1,6 +1,77 @@
1
1
  import { z } from "zod";
2
2
  import { type GateName } from "../graph/schema.js";
3
3
  import { type GateOutcome } from "./outcome.js";
4
+ /** Shared lifecycle vocabulary for shell evidence and later build-result emitters. */
5
+ export declare const COMMAND_RECEIPT_OUTCOMES: readonly ["started", "completed", "spawn-failed", "timed-out", "cancelled", "reused-result", "skipped", "refused"];
6
+ export declare const CommandReceiptAttributionSchema: z.ZodObject<{
7
+ runId: z.ZodString;
8
+ taskId: z.ZodString;
9
+ attempt: z.ZodNumber;
10
+ gateRound: z.ZodNumber;
11
+ invocation: z.ZodString;
12
+ }, z.core.$strict>;
13
+ export type CommandReceiptAttribution = z.infer<typeof CommandReceiptAttributionSchema>;
14
+ export declare const ShellReceiptSchema: z.ZodObject<{
15
+ outcome: z.ZodEnum<{
16
+ skipped: "skipped";
17
+ started: "started";
18
+ completed: "completed";
19
+ "spawn-failed": "spawn-failed";
20
+ "timed-out": "timed-out";
21
+ cancelled: "cancelled";
22
+ "reused-result": "reused-result";
23
+ refused: "refused";
24
+ }>;
25
+ confirmedStart: z.ZodBoolean;
26
+ attribution: z.ZodOptional<z.ZodObject<{
27
+ runId: z.ZodString;
28
+ taskId: z.ZodString;
29
+ attempt: z.ZodNumber;
30
+ gateRound: z.ZodNumber;
31
+ invocation: z.ZodString;
32
+ }, z.core.$strict>>;
33
+ pid: z.ZodOptional<z.ZodNumber>;
34
+ exitCode: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
35
+ signal: z.ZodOptional<z.ZodNullable<z.ZodString>>;
36
+ error: z.ZodOptional<z.ZodString>;
37
+ durationMs: z.ZodOptional<z.ZodNumber>;
38
+ }, z.core.$strict>;
39
+ export type ShellReceipt = z.infer<typeof ShellReceiptSchema>;
40
+ /** A task-build row must carry the entire caller-owned correlation, even before spawn. */
41
+ export declare const CommandReceiptSchema: z.ZodObject<{
42
+ outcome: z.ZodEnum<{
43
+ skipped: "skipped";
44
+ started: "started";
45
+ completed: "completed";
46
+ "spawn-failed": "spawn-failed";
47
+ "timed-out": "timed-out";
48
+ cancelled: "cancelled";
49
+ "reused-result": "reused-result";
50
+ refused: "refused";
51
+ }>;
52
+ confirmedStart: z.ZodBoolean;
53
+ pid: z.ZodOptional<z.ZodNumber>;
54
+ exitCode: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
55
+ signal: z.ZodOptional<z.ZodNullable<z.ZodString>>;
56
+ error: z.ZodOptional<z.ZodString>;
57
+ durationMs: z.ZodOptional<z.ZodNumber>;
58
+ attribution: z.ZodObject<{
59
+ runId: z.ZodString;
60
+ taskId: z.ZodString;
61
+ attempt: z.ZodNumber;
62
+ gateRound: z.ZodNumber;
63
+ invocation: z.ZodString;
64
+ }, z.core.$strict>;
65
+ }, z.core.$strict>;
66
+ export type CommandReceipt = z.infer<typeof CommandReceiptSchema>;
67
+ export declare function readCommandReceipt(raw: unknown): {
68
+ kind: "receipt";
69
+ receipt: CommandReceipt;
70
+ } | {
71
+ kind: "protocol-issue";
72
+ issues: string[];
73
+ raw: unknown;
74
+ };
4
75
  /** The T31 outcome vocabulary, made executable and strict at the persistence boundary. */
5
76
  export declare const GateOutcomeSchema: z.ZodType<GateOutcome>;
6
77
  export declare const ROLE_INVOCATION_ROLES: readonly ["worker", "judge", "review", "consult"];
@@ -12,6 +12,38 @@ const TaskIdSchema = NonEmptyStringSchema;
12
12
  const TaskIdsSchema = z.array(TaskIdSchema);
13
13
  const AttemptSchema = z.number().int().nonnegative();
14
14
  const EvidenceSchema = z.record(z.string(), z.unknown());
15
+ /** Shared lifecycle vocabulary for shell evidence and later build-result emitters. */
16
+ export const COMMAND_RECEIPT_OUTCOMES = [
17
+ "started", "completed", "spawn-failed", "timed-out", "cancelled", "reused-result", "skipped", "refused",
18
+ ];
19
+ export const CommandReceiptAttributionSchema = z.object({
20
+ runId: NonEmptyStringSchema,
21
+ taskId: TaskIdSchema,
22
+ attempt: AttemptSchema,
23
+ gateRound: z.number().int().nonnegative(),
24
+ invocation: NonEmptyStringSchema,
25
+ }).strict();
26
+ export const ShellReceiptSchema = z.object({
27
+ outcome: z.enum(COMMAND_RECEIPT_OUTCOMES),
28
+ confirmedStart: z.boolean(),
29
+ attribution: CommandReceiptAttributionSchema.optional(),
30
+ pid: z.number().int().positive().optional(),
31
+ exitCode: z.number().int().nullable().optional(),
32
+ signal: NonEmptyStringSchema.nullable().optional(),
33
+ error: z.string().optional(),
34
+ durationMs: z.number().nonnegative().optional(),
35
+ }).strict();
36
+ /** A task-build row must carry the entire caller-owned correlation, even before spawn. */
37
+ export const CommandReceiptSchema = ShellReceiptSchema.extend({
38
+ attribution: CommandReceiptAttributionSchema,
39
+ });
40
+ export function readCommandReceipt(raw) {
41
+ const parsed = CommandReceiptSchema.safeParse(raw);
42
+ return parsed.success ? { kind: "receipt", receipt: parsed.data } : {
43
+ kind: "protocol-issue", raw,
44
+ issues: parsed.error.issues.map((issue) => `${issue.path.join(".")}: ${issue.message}`),
45
+ };
46
+ }
15
47
  /** The T31 outcome vocabulary, made executable and strict at the persistence boundary. */
16
48
  export const GateOutcomeSchema = z.discriminatedUnion("kind", [
17
49
  z.object({ kind: z.literal("passed") }).strict(),
@@ -0,0 +1,8 @@
1
+ import type { GateResult } from "../gates/types.js";
2
+ import type { JournalEvent } from "./journal.js";
3
+ export type FailureDisposition = "infrastructure" | "behavioral" | "unknown";
4
+ export type VerificationRetryCause = "infrastructure" | "host-starved";
5
+ /** Structured verifier evidence wins; neither an exit code nor prose proves infrastructure. */
6
+ export declare function failureDisposition(result: Pick<GateResult, "pass" | "meta">): FailureDisposition;
7
+ /** Reserve before re-execution. Interrupted reservations and new subjects never refill the task cap. */
8
+ export declare function reserveInfrastructureRetry(events: readonly JournalEvent[], taskId: string, subject: string, append: (event: string, taskId: string, data: Record<string, unknown>) => void, cause?: VerificationRetryCause): boolean;
@@ -0,0 +1,25 @@
1
+ /** Structured verifier evidence wins; neither an exit code nor prose proves infrastructure. */
2
+ export function failureDisposition(result) {
3
+ if (result.pass)
4
+ return "unknown";
5
+ if (result.meta?.classification === "regression")
6
+ return "behavioral";
7
+ if (result.meta?.infra === true && result.meta?.classification === "infra")
8
+ return "infrastructure";
9
+ return "unknown";
10
+ }
11
+ /** Reserve before re-execution. Interrupted reservations and new subjects never refill the task cap. */
12
+ export function reserveInfrastructureRetry(events, taskId, subject, append, cause = "infrastructure") {
13
+ const used = events.filter((e) => e.taskId === taskId && e.event === "infra-retry-reserved");
14
+ const malformed = used.some((e) => typeof e.data.subject !== "string" || !e.data.subject
15
+ || (e.data.cause !== "infrastructure" && e.data.cause !== "host-starved"));
16
+ const reason = !subject ? "unknown-subject" : malformed ? "invalid-accounting"
17
+ : used.some((e) => e.data.subject === subject) ? "subject-allowance-exhausted"
18
+ : used.length >= 2 ? "task-allowance-exhausted" : undefined;
19
+ if (reason) {
20
+ append("infra-retry-denied", taskId, { subject, cause, reason, consumed: used.length });
21
+ return false;
22
+ }
23
+ append("infra-retry-reserved", taskId, { subject, cause, consumed: used.length + 1 });
24
+ return true;
25
+ }
@@ -0,0 +1,12 @@
1
+ import type { JournalEvent } from "./journal.js";
2
+ export interface RepairSelectionDecision {
3
+ selectTests: boolean;
4
+ requiredFiles: string[];
5
+ reason: "no-test-failure" | "known-failing-files" | "legacy-test-failure" | "unattributed-test-failure" | "full-suite-failure";
6
+ }
7
+ type SelectionEvent = Pick<JournalEvent, "event" | "taskId" | "data">;
8
+ /** Diagnostic selection only: neither a narrow pass nor this decision can authorize integration.
9
+ * Distrust and known failing files survive later green results, approvals and resume. A historical
10
+ * ambiguous failure is never reinterpreted using a newly enabled policy. */
11
+ export declare function repairSelectionDecision(events: readonly SelectionEvent[], taskId: string, enabled: boolean): RepairSelectionDecision;
12
+ export {};
@@ -0,0 +1,56 @@
1
+ /** These are literal repository-relative identities. Existence and selection support are checked
2
+ * against the current worktree by the gate; this journal fold cannot establish either. */
3
+ function fileIdentities(value) {
4
+ if (!Array.isArray(value) || value.length === 0 || value.some((file) => typeof file !== "string" || !file || /[\\\x00-\x1f\x7f]/.test(file)
5
+ || /^[A-Za-z]:/.test(file) || file.split("/").some((part) => !part || part === "." || part === ".."))) {
6
+ return undefined;
7
+ }
8
+ return [...new Set(value)].sort();
9
+ }
10
+ /** Diagnostic selection only: neither a narrow pass nor this decision can authorize integration.
11
+ * Distrust and known failing files survive later green results, approvals and resume. A historical
12
+ * ambiguous failure is never reinterpreted using a newly enabled policy. */
13
+ export function repairSelectionDecision(events, taskId, enabled) {
14
+ const required = new Set();
15
+ let distrust;
16
+ for (const event of events) {
17
+ if (event.taskId !== taskId || event.event !== "gate-result"
18
+ || event.data.gate !== "test" || event.data.pass !== false)
19
+ continue;
20
+ const data = event.data;
21
+ if (!enabled) {
22
+ distrust ??= "legacy-test-failure";
23
+ continue;
24
+ }
25
+ // Only the recorded closed disposition can exempt infrastructure. Contradictory structured
26
+ // failure evidence cannot turn a behavioral failure into a harmless infrastructure event.
27
+ if (data.disposition === "infrastructure" && data.classification !== "regression"
28
+ && (data.failingFiles === undefined || (Array.isArray(data.failingFiles) && data.failingFiles.length === 0)))
29
+ continue;
30
+ if (data.disposition !== "behavioral" || typeof data.commit !== "string"
31
+ || !/^(?:[a-f0-9]{40}|[a-f0-9]{64})$/i.test(data.commit)) {
32
+ distrust ??= "unattributed-test-failure";
33
+ continue;
34
+ }
35
+ // A replacement full-suite failure follows a passing screen in the current pipeline. No
36
+ // ordinary full-suite failure establishes that a selector safely covers the failed behavior.
37
+ if (data.fullSuite === true) {
38
+ distrust ??= "full-suite-failure";
39
+ continue;
40
+ }
41
+ const selected = fileIdentities(data.selectedTests);
42
+ const failing = fileIdentities(data.failingFiles);
43
+ if (!selected || !failing || failing.some((file) => !selected.includes(file))
44
+ || (data.fullSuite !== undefined && data.fullSuite !== false)) {
45
+ distrust ??= "unattributed-test-failure";
46
+ continue;
47
+ }
48
+ for (const file of failing)
49
+ required.add(file);
50
+ }
51
+ return {
52
+ selectTests: distrust === undefined,
53
+ requiredFiles: [...required].sort(),
54
+ reason: distrust ?? (required.size ? "known-failing-files" : "no-test-failure"),
55
+ };
56
+ }
@@ -10,6 +10,10 @@ export declare function harvestCpuFlatWindowMs(resolutionMs: number): number;
10
10
  /** Test seam — pin the quantization-aware flat window without changing production policy. */
11
11
  export declare function setHarvestCpuFlatMsForTests(ms: number): void;
12
12
  export declare function resetHarvestCpuFlatMsForTests(): void;
13
+ /** The dispatch shell records its foreground group in an attempt-unique artifact. */
14
+ export declare function readOwnedProcessGroup(path: string): number | undefined;
15
+ /** Never infer ownership from a task name or scan another attempt's marker when reaping. */
16
+ export declare function reapOwnedProcessGroup(group: number | undefined, cwd: string): Promise<number[] | null>;
13
17
  export declare function workerTreeCpuMs(marker: string, cwd: string): Promise<{
14
18
  ms: number;
15
19
  resolutionMs: number;
@@ -17,6 +21,7 @@ export declare function workerTreeCpuMs(marker: string, cwd: string): Promise<{
17
21
  export declare class WorkerTreeCpuAccountant {
18
22
  private marker;
19
23
  private cwd;
24
+ private group?;
20
25
  private active;
21
26
  private loop;
22
27
  private live;
@@ -24,7 +29,7 @@ export declare class WorkerTreeCpuAccountant {
24
29
  private gaps;
25
30
  private consecutiveGaps;
26
31
  private latest;
27
- constructor(marker: string, cwd: string);
32
+ constructor(marker: string, cwd: string, group?: (() => number | undefined) | undefined);
28
33
  private sample;
29
34
  start(): Promise<void>;
30
35
  read(): {
package/dist/run/stall.js CHANGED
@@ -52,6 +52,51 @@ function parsePsCpu(raw) {
52
52
  + Math.round(Number(m[4]) * 1000);
53
53
  return { ms, frac: m[4].includes(".") };
54
54
  }
55
+ /** The dispatch shell records its foreground group in an attempt-unique artifact. */
56
+ export function readOwnedProcessGroup(path) {
57
+ try {
58
+ const raw = readFileSync(path, "utf8").trim();
59
+ const group = /^\d+$/.test(raw) ? Number(raw) : 0;
60
+ return Number.isSafeInteger(group) && group > 1 ? group : undefined;
61
+ }
62
+ catch {
63
+ return undefined;
64
+ }
65
+ }
66
+ /** Never infer ownership from a task name or scan another attempt's marker when reaping. */
67
+ export async function reapOwnedProcessGroup(group, cwd) {
68
+ if (group === undefined)
69
+ return null;
70
+ const own = await shGit(`ps -o pgid= -p ${process.pid}`, cwd, 5_000);
71
+ // An in-process driver fixture (or a non-isolating host) can share the daemon's
72
+ // group. It is not an owned worker group: let the driver's close retire that slot.
73
+ if (own.code !== 0 || !/^\d+$/.test(own.stdout.trim()) || Number(own.stdout.trim()) === group)
74
+ return null;
75
+ try {
76
+ process.kill(-group, "SIGKILL");
77
+ }
78
+ catch (error) {
79
+ if (error.code !== "ESRCH")
80
+ throw error;
81
+ }
82
+ let survivors = [];
83
+ // SIGKILL delivery and process retirement are asynchronous. Bound the confirmation
84
+ // grace, retaining names if a process is still present after it.
85
+ for (let probe = 0; probe < 5; probe++) {
86
+ const snapshot = await shGit("ps -Awwo pid=,pgid=,stat=", cwd, 5_000);
87
+ if (snapshot.code !== 0)
88
+ return null;
89
+ survivors = snapshot.stdout.split("\n").flatMap((line) => {
90
+ const row = /^\s*(\d+)\s+(\d+)\s+(\S+)/.exec(line);
91
+ return row && Number(row[2]) === group && !row[3].startsWith("Z") ? [Number(row[1])] : [];
92
+ });
93
+ if (survivors.length === 0)
94
+ break;
95
+ if (probe < 4)
96
+ await new Promise((resolve) => setTimeout(resolve, 50));
97
+ }
98
+ return survivors;
99
+ }
55
100
  let linuxClockTickMs;
56
101
  function linuxProcessCpuMs(pid, cwd) {
57
102
  if (!existsSync("/proc/self/stat"))
@@ -81,7 +126,7 @@ function linuxProcessCpuMs(pid, cwd) {
81
126
  // Every non-seeded worker process descends from its attempt-unique dispatch script. One ps snapshot
82
127
  // finds that root and its descendants. An empty tree is measurable zero; a failed or unparseable
83
128
  // snapshot is undefined because missing evidence can never prove inactivity.
84
- async function workerTreeCpuSnapshot(marker, cwd) {
129
+ async function workerTreeCpuSnapshot(marker, cwd, group) {
85
130
  const snapshot = await shGit("ps -Awwo pid=,ppid=,time=,command=", cwd, 15_000);
86
131
  if (snapshot.code !== 0)
87
132
  return undefined;
@@ -97,6 +142,16 @@ async function workerTreeCpuSnapshot(marker, cwd) {
97
142
  if (rows.length === 0)
98
143
  return undefined;
99
144
  const tree = new Set(rows.filter((p) => p.cmd.includes(marker)).map((p) => p.pid));
145
+ if (group !== undefined) {
146
+ const groups = await shGit("ps -Awwo pid=,pgid=", cwd, 15_000);
147
+ if (groups.code !== 0)
148
+ return undefined;
149
+ for (const line of groups.stdout.split("\n")) {
150
+ const row = /^\s*(\d+)\s+(\d+)\s*$/.exec(line);
151
+ if (row && Number(row[2]) === group)
152
+ tree.add(row[1]);
153
+ }
154
+ }
100
155
  // ps output is not topologically ordered; relax the parent -> child closure until stable.
101
156
  for (let grew = true; grew;) {
102
157
  grew = false;
@@ -141,6 +196,7 @@ export async function workerTreeCpuMs(marker, cwd) {
141
196
  export class WorkerTreeCpuAccountant {
142
197
  marker;
143
198
  cwd;
199
+ group;
144
200
  active = false;
145
201
  loop;
146
202
  live = new Map();
@@ -148,12 +204,13 @@ export class WorkerTreeCpuAccountant {
148
204
  gaps = 0;
149
205
  consecutiveGaps = 0;
150
206
  latest;
151
- constructor(marker, cwd) {
207
+ constructor(marker, cwd, group) {
152
208
  this.marker = marker;
153
209
  this.cwd = cwd;
210
+ this.group = group;
154
211
  }
155
212
  async sample() {
156
- const snapshot = await workerTreeCpuSnapshot(this.marker, this.cwd);
213
+ const snapshot = await workerTreeCpuSnapshot(this.marker, this.cwd, this.group?.());
157
214
  if (snapshot === undefined) {
158
215
  this.gaps++;
159
216
  this.live.clear();
@@ -1,5 +1,6 @@
1
1
  import { type RunGraph } from "../../graph/schema.js";
2
2
  import type { OperatorSnapshot } from "../../run/operator-state.js";
3
+ import type { AttemptHarvest, TaskRow } from "./derive.js";
3
4
  /** The prototype's palette (tasks-redesign.mjs:24-30): xterm-256 indices, stated once. */
4
5
  export declare const BOARD_PALETTE: {
5
6
  readonly chrome: 238;
@@ -49,6 +50,8 @@ export interface BoardInput {
49
50
  /** Footer key hints, joined the prototype's way; `false` leaves the footer to the caller (see boardFooter). */
50
51
  readonly keys?: readonly string[] | false;
51
52
  readonly colour?: boolean;
53
+ /** The cockpit's derived rows: the shared projection (blocker, next action, harvest) rides on each board row, unrendered. */
54
+ readonly rows?: readonly TaskRow[];
52
55
  }
53
56
  /** One task row, in the parts the Run view recomposes per width band. */
54
57
  export interface BoardRow {
@@ -63,6 +66,12 @@ export interface BoardRow {
63
66
  readonly note: string;
64
67
  /** The note's segments, so a band can wrap it across continuation rows without cutting one. */
65
68
  readonly noteParts: readonly string[];
69
+ /** From the shared projection (derive.ts TaskRow.summary), when the caller supplied rows. */
70
+ readonly blocker?: string;
71
+ readonly nextAction?: string;
72
+ readonly permittedActions?: readonly string[];
73
+ /** OBS-1048: the newest recorded attempt's harvest evidence, when the caller supplied rows. */
74
+ readonly harvest?: AttemptHarvest;
66
75
  }
67
76
  export interface BoardFrame {
68
77
  /** Header through the gate header rule, in the prototype's order. */
@@ -89,6 +89,15 @@ export function areasFor(files) {
89
89
  const hm = (ms) => `${Math.floor(ms / 3.6e6)}h${String(Math.floor((ms % 3.6e6) / 6e4)).padStart(2, "0")}m`;
90
90
  const ago = (ms) => (ms < 6e4 ? "just now" : ms < 3.6e6 ? `${Math.round(ms / 6e4)}m ago` : `${Math.round(ms / 3.6e6)}h ago`);
91
91
  const isDone = (t) => t !== undefined && (t.merged || t.state === "completed");
92
+ const projected = (row) => {
93
+ const blocker = row?.summary?.blocker;
94
+ const harvest = row?.harvests?.at(-1);
95
+ return {
96
+ ...(blocker ? { blocker: blocker.kind, permittedActions: blocker.permittedActions } : {}),
97
+ ...(blocker?.nextAction ? { nextAction: blocker.nextAction } : {}),
98
+ ...(harvest ? { harvest } : {}),
99
+ };
100
+ };
92
101
  /** The prototype's frame, sectioned. `renderBoard` joins it row for row. */
93
102
  export function boardFrame(input, width) {
94
103
  const W = Math.max(1, Math.floor(width));
@@ -218,6 +227,7 @@ export function boardFrame(input, width) {
218
227
  tail: ` ${pad(mute(sliceCells(t?.channel ?? "—", CHAN - 2).head), CHAN)}${pad(mute(String(attempts)), 5)}`,
219
228
  note: note.join(mute(" · ")),
220
229
  noteParts: note,
230
+ ...projected(input.rows?.find((row) => row.taskId === g.id)),
221
231
  });
222
232
  }
223
233
  const bottom = [];
@@ -1,4 +1,6 @@
1
1
  export { readOperatorState as deriveOperatorState } from "../../run/operator-state.js";
2
+ import { type TaskActivityProjection } from "../../run/activity.js";
3
+ import { type OperatorTaskSummary } from "../../run/operator-summary.js";
2
4
  import type { ComponentState, JournalRow, StatusStripItem } from "./components.js";
3
5
  import type { RunViewId } from "./views.js";
4
6
  export declare const SPARKLINE_BUCKET_WINDOW = 12;
@@ -60,8 +62,34 @@ export type CockpitGraph = {
60
62
  readonly id: string;
61
63
  readonly title?: string;
62
64
  readonly status?: string;
65
+ /** Prerequisites and declared gates: read only by the shared projection (blocker, readiness). */
66
+ readonly deps?: readonly string[];
67
+ readonly gates?: readonly string[];
63
68
  }[];
64
69
  };
70
+ /** One recorded seat: a task attempt in a role. Keyed by all three, never by provider alone. */
71
+ export type RecordedIdentity = {
72
+ readonly key: string;
73
+ readonly role: string;
74
+ readonly attempt: number;
75
+ /** The slot name the driver recorded, verbatim; absent when the row named none. */
76
+ readonly name?: string;
77
+ };
78
+ /**
79
+ * OBS-1048: what the journal recorded about one in-flight attempt's harvest. `trailerFirstSeenAt`
80
+ * is always absent — no producer journals it (daemon.ts samples the trailer without recording;
81
+ * OBS-1048 add.1, 2.5.8) — and is never synthesized from any other timestamp.
82
+ */
83
+ export type AttemptHarvest = {
84
+ readonly attempt: number;
85
+ readonly lastWorkerStatus?: string;
86
+ readonly nudgeFailures: number;
87
+ readonly lastNudgeFailureAt?: string;
88
+ readonly pageCount: number;
89
+ readonly trailerFirstSeenAt?: undefined;
90
+ /** SUSPECTED: nudges failed and pages fired, and no worker-result of THIS attempt followed the launch. */
91
+ readonly suspectedStalledHarvest: boolean;
92
+ };
65
93
  /** How an unrecorded field draws: the engagement said nothing, so the row says nothing. */
66
94
  export declare const ABSENT_FIELD = "-";
67
95
  /**
@@ -105,6 +133,13 @@ export type TaskRow = {
105
133
  * that earns one rather than having to read `done` as if it were the same.
106
134
  */
107
135
  readonly merged?: true;
136
+ /** Evidence-only activity projection (run/activity.ts), the one status renders. */
137
+ readonly activity?: TaskActivityProjection;
138
+ /** Operator summary (phase, blocker, next action) — the same projection status renders. */
139
+ readonly summary?: OperatorTaskSummary;
140
+ readonly identities?: readonly RecordedIdentity[];
141
+ /** Per recorded attempt, newest last (OBS-1048). */
142
+ readonly harvests?: readonly AttemptHarvest[];
108
143
  };
109
144
  /** One recorded gate result. `details` is the record's own text, verbatim. */
110
145
  export type GateRow = {