instar 0.28.95 → 0.28.97
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/dashboard/index.html +249 -2
- package/dist/commands/server.d.ts.map +1 -1
- package/dist/commands/server.js +90 -1
- package/dist/commands/server.js.map +1 -1
- package/dist/core/MachineHeartbeat.d.ts +94 -0
- package/dist/core/MachineHeartbeat.d.ts.map +1 -0
- package/dist/core/MachineHeartbeat.js +155 -0
- package/dist/core/MachineHeartbeat.js.map +1 -0
- package/dist/core/ProjectAutoAdvancePoller.d.ts +107 -0
- package/dist/core/ProjectAutoAdvancePoller.d.ts.map +1 -0
- package/dist/core/ProjectAutoAdvancePoller.js +176 -0
- package/dist/core/ProjectAutoAdvancePoller.js.map +1 -0
- package/dist/core/ProjectRoundCompleteMessage.d.ts +158 -0
- package/dist/core/ProjectRoundCompleteMessage.d.ts.map +1 -0
- package/dist/core/ProjectRoundCompleteMessage.js +253 -0
- package/dist/core/ProjectRoundCompleteMessage.js.map +1 -0
- package/dist/core/ProjectRoundExecution.d.ts +111 -0
- package/dist/core/ProjectRoundExecution.d.ts.map +1 -0
- package/dist/core/ProjectRoundExecution.js +359 -0
- package/dist/core/ProjectRoundExecution.js.map +1 -0
- package/dist/core/ProjectRoundLock.d.ts +67 -0
- package/dist/core/ProjectRoundLock.d.ts.map +1 -0
- package/dist/core/ProjectRoundLock.js +161 -0
- package/dist/core/ProjectRoundLock.js.map +1 -0
- package/dist/core/ProjectRoundRunner.d.ts +146 -0
- package/dist/core/ProjectRoundRunner.d.ts.map +1 -0
- package/dist/core/ProjectRoundRunner.js +371 -0
- package/dist/core/ProjectRoundRunner.js.map +1 -0
- package/dist/core/ProjectRoundWorktrees.d.ts +57 -0
- package/dist/core/ProjectRoundWorktrees.d.ts.map +1 -0
- package/dist/core/ProjectRoundWorktrees.js +111 -0
- package/dist/core/ProjectRoundWorktrees.js.map +1 -0
- package/dist/server/AgentServer.d.ts +12 -0
- package/dist/server/AgentServer.d.ts.map +1 -1
- package/dist/server/AgentServer.js +3 -0
- package/dist/server/AgentServer.js.map +1 -1
- package/dist/server/routes.d.ts +19 -0
- package/dist/server/routes.d.ts.map +1 -1
- package/dist/server/routes.js +782 -7
- package/dist/server/routes.js.map +1 -1
- package/package.json +1 -1
- package/src/data/builtin-manifest.json +48 -48
- package/upgrades/0.28.96.md +347 -0
- package/upgrades/0.28.97.md +146 -0
- package/upgrades/side-effects/project-scope-phase1-7-skill-surface.md +175 -0
- package/upgrades/side-effects/project-scope-phase1b-connect-the-dots.md +184 -0
- package/upgrades/side-effects/project-scope-phase1b-pr3.md +228 -0
- package/upgrades/side-effects/project-scope-phase1b-pr4.md +198 -0
- package/upgrades/side-effects/project-scope-phase1b-pr5.md +150 -0
- package/upgrades/side-effects/project-scope-phase1b-pr6.md +136 -0
- package/upgrades/side-effects/project-scope-phase1b-pr7.md +175 -0
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ProjectRoundRunner — the single entry point for project-scope rounds.
|
|
3
|
+
*
|
|
4
|
+
* Spec: docs/specs/PROJECT-SCOPE-SPEC.md § Phase 1.5.
|
|
5
|
+
*
|
|
6
|
+
* Authority model: every entry path (HTTP /advance, /run-round skill,
|
|
7
|
+
* future auto-advance poller) MUST go through `preflight()` here. Routes
|
|
8
|
+
* do NOT enforce gates that the runner already enforces — the runner is
|
|
9
|
+
* the chokepoint. Preflight returns a structured result; callers either
|
|
10
|
+
* proceed (when the runner approves) or surface the rejection.
|
|
11
|
+
*
|
|
12
|
+
* Lock: machine-local `.instar/local/round-runner.lock` (see
|
|
13
|
+
* `ProjectRoundLock`). At most one runner active per machine.
|
|
14
|
+
*
|
|
15
|
+
* What's IN this PR (Phase 1b PR 3):
|
|
16
|
+
* - `preflight(projectId, roundIndex)` — checks 1–9 from the spec
|
|
17
|
+
* (drift check, step 10, is deferred to a follow-up PR alongside
|
|
18
|
+
* the cache+ledger HTTP wiring).
|
|
19
|
+
* - `halt(projectId, reason)` — writes haltedAt, sets project status
|
|
20
|
+
* to `halted`, releases the lock if held. Idempotent.
|
|
21
|
+
* - `recordAck(projectId, roundIndex)` — records user acknowledgment,
|
|
22
|
+
* resets `unacknowledgedAdvanceCount`, advances `lastAckedRoundIndex`,
|
|
23
|
+
* populates `firstLaunchAckAt` if absent.
|
|
24
|
+
* - `acceptPartial(projectId, roundIndex, reason)` — closes a
|
|
25
|
+
* partially-complete round: missing items → skipped,
|
|
26
|
+
* round.status → `complete-with-skips`. Counts as an ack for the
|
|
27
|
+
* current round (advances lastAckedRoundIndex) but does NOT
|
|
28
|
+
* increment unacknowledgedAdvanceCount.
|
|
29
|
+
*
|
|
30
|
+
* What's NOT in this PR:
|
|
31
|
+
* - The actual autonomous-delegating run loop (`run()`).
|
|
32
|
+
* - Dynamic stop-condition revalidation + SIGTERM/SIGKILL of children.
|
|
33
|
+
* - Drift check at preflight step 10.
|
|
34
|
+
* - Auto-advance poller.
|
|
35
|
+
* - Multi-machine claim-ownership.
|
|
36
|
+
*/
|
|
37
|
+
import type { InitiativeTracker, Initiative, InitiativeRound } from './InitiativeTracker.js';
|
|
38
|
+
import { type ProjectRoundLockPayload } from './ProjectRoundLock.js';
|
|
39
|
+
/** Spec-mandated rejection codes for preflight failures. */
|
|
40
|
+
export type PreflightRejectCode = 'TRACKER_MISSING' | 'PROJECT_NOT_FOUND' | 'PROJECT_NOT_PROJECT_KIND' | 'PROJECT_INACTIVE' | 'PROJECT_HALTED' | 'PROJECT_AWAITING_USER' | 'ROUND_INDEX_OUT_OF_RANGE' | 'ROUND_NOT_PENDING_OR_READY' | 'LOCK_HELD' | 'FIRST_LAUNCH_ACK_REQUIRED' | 'UNACKED_ADVANCES_OVER_CAP' | 'ROUND_ACK_GAP_TOO_LARGE' | 'NOT_OWNER_MACHINE' | 'TARGET_REPO_PATH_INVALID' | 'AWAITING_RECONCILIATION' | 'ITEMS_NOT_ALL_APPROVED' | 'ITEM_FRONTMATTER_INVALID';
|
|
41
|
+
export interface PreflightOk {
|
|
42
|
+
ok: true;
|
|
43
|
+
project: Initiative;
|
|
44
|
+
round: InitiativeRound;
|
|
45
|
+
children: Initiative[];
|
|
46
|
+
ownerMachineId: string;
|
|
47
|
+
}
|
|
48
|
+
export interface PreflightReject {
|
|
49
|
+
ok: false;
|
|
50
|
+
code: PreflightRejectCode;
|
|
51
|
+
reason: string;
|
|
52
|
+
/** When code === 'LOCK_HELD', the current holder is populated. */
|
|
53
|
+
currentHolder?: ProjectRoundLockPayload;
|
|
54
|
+
/** When code === 'ITEMS_NOT_ALL_APPROVED', the failing child id(s). */
|
|
55
|
+
failingItemIds?: string[];
|
|
56
|
+
}
|
|
57
|
+
export type PreflightResult = PreflightOk | PreflightReject;
|
|
58
|
+
export interface ProjectRoundRunnerConfig {
|
|
59
|
+
tracker: InitiativeTracker;
|
|
60
|
+
/** Absolute path to the agent's `.instar/` directory. */
|
|
61
|
+
stateDir: string;
|
|
62
|
+
/** Identifier of THIS machine — usually `agentRegistry.machineId()`. */
|
|
63
|
+
machineId: string;
|
|
64
|
+
/**
|
|
65
|
+
* How many auto-advances without ack before the project is paused.
|
|
66
|
+
* Defaults to 2 (matches spec § Phase 1.5 step 6 brake).
|
|
67
|
+
*/
|
|
68
|
+
unackedAdvanceCap?: number;
|
|
69
|
+
/**
|
|
70
|
+
* Max gap allowed between `lastAckedRoundIndex` and the requested
|
|
71
|
+
* roundIndex. Defaults to 2 (spec § Phase 1.5 step 5: "no more than
|
|
72
|
+
* two rounds-ahead-of-ack at any time").
|
|
73
|
+
*/
|
|
74
|
+
ackGapCap?: number;
|
|
75
|
+
}
|
|
76
|
+
export declare class ProjectRoundRunner {
|
|
77
|
+
private tracker;
|
|
78
|
+
private stateDir;
|
|
79
|
+
private machineId;
|
|
80
|
+
private lock;
|
|
81
|
+
private unackedAdvanceCap;
|
|
82
|
+
private ackGapCap;
|
|
83
|
+
constructor(config: ProjectRoundRunnerConfig);
|
|
84
|
+
/** Public read of the current lock holder, used by tests and dashboards. */
|
|
85
|
+
currentLockHolder(): ProjectRoundLockPayload | null;
|
|
86
|
+
/**
|
|
87
|
+
* Preflight check — the single chokepoint for round-start authority.
|
|
88
|
+
*
|
|
89
|
+
* Steps 1–9 from the spec. Step 10 (drift check) is intentionally
|
|
90
|
+
* skipped in this PR; it lands when the drift-check HTTP endpoint is
|
|
91
|
+
* wired into the routes ctx (Phase 1b follow-up).
|
|
92
|
+
*/
|
|
93
|
+
preflight(projectId: string, roundIndex: number): PreflightResult;
|
|
94
|
+
/**
|
|
95
|
+
* Halt the active round of a project. Idempotent — halting an already-
|
|
96
|
+
* halted round is a no-op. Releases the lock if the calling machine
|
|
97
|
+
* holds it.
|
|
98
|
+
*
|
|
99
|
+
* Returns the round that was halted, or null if the project / round
|
|
100
|
+
* didn't exist.
|
|
101
|
+
*/
|
|
102
|
+
halt(projectId: string, reason: string): Promise<{
|
|
103
|
+
project: Initiative;
|
|
104
|
+
roundIndex: number;
|
|
105
|
+
} | null>;
|
|
106
|
+
/**
|
|
107
|
+
* Record a user acknowledgment for `roundIndex`. Idempotent: calling
|
|
108
|
+
* twice with the same index has no further effect. The fields the spec
|
|
109
|
+
* names: resets `unacknowledgedAdvanceCount`, populates
|
|
110
|
+
* `firstLaunchAckAt` (if absent), advances `lastAckedRoundIndex`.
|
|
111
|
+
*/
|
|
112
|
+
recordAck(projectId: string, roundIndex: number): Promise<Initiative | null>;
|
|
113
|
+
/**
|
|
114
|
+
* Close a partially-complete round: missing items → skipped, round
|
|
115
|
+
* status → `complete-with-skips`. Counts as an ack for `roundIndex`
|
|
116
|
+
* (advances `lastAckedRoundIndex`) but does NOT touch
|
|
117
|
+
* `unacknowledgedAdvanceCount`.
|
|
118
|
+
*
|
|
119
|
+
* The transition of each missing item to `skipped` requires `reason`
|
|
120
|
+
* and `skippedBy` (per StageTransitionValidator). The caller passes
|
|
121
|
+
* a single `reason` that's applied to every newly-skipped item.
|
|
122
|
+
*/
|
|
123
|
+
acceptPartial(projectId: string, roundIndex: number, reason: string, skippedBy: string): Promise<{
|
|
124
|
+
project: Initiative;
|
|
125
|
+
skippedItemIds: string[];
|
|
126
|
+
} | null>;
|
|
127
|
+
/**
|
|
128
|
+
* Validate that a child initiative's spec frontmatter still has the
|
|
129
|
+
* tags the round-runner requires before STARTing the autonomous loop.
|
|
130
|
+
* Spec § Phase 1.5 step 3 + § Phase 1.4 "Authority separation":
|
|
131
|
+
* - `review-convergence: true`
|
|
132
|
+
* - `approved: true`
|
|
133
|
+
*
|
|
134
|
+
* Used by future `run()` and by the dashboard's "ready to start" badge.
|
|
135
|
+
* Exported for tests.
|
|
136
|
+
*/
|
|
137
|
+
static validateChildFrontmatter(targetRepoPath: string, specRelPath: string): {
|
|
138
|
+
ok: true;
|
|
139
|
+
} | {
|
|
140
|
+
ok: false;
|
|
141
|
+
reason: string;
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
/** Convenience alias for the lock primitive when callers want a typed reference. */
|
|
145
|
+
export type { ProjectRoundLockPayload } from './ProjectRoundLock.js';
|
|
146
|
+
//# sourceMappingURL=ProjectRoundRunner.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ProjectRoundRunner.d.ts","sourceRoot":"","sources":["../../src/core/ProjectRoundRunner.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,371 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ProjectRoundRunner — the single entry point for project-scope rounds.
|
|
3
|
+
*
|
|
4
|
+
* Spec: docs/specs/PROJECT-SCOPE-SPEC.md § Phase 1.5.
|
|
5
|
+
*
|
|
6
|
+
* Authority model: every entry path (HTTP /advance, /run-round skill,
|
|
7
|
+
* future auto-advance poller) MUST go through `preflight()` here. Routes
|
|
8
|
+
* do NOT enforce gates that the runner already enforces — the runner is
|
|
9
|
+
* the chokepoint. Preflight returns a structured result; callers either
|
|
10
|
+
* proceed (when the runner approves) or surface the rejection.
|
|
11
|
+
*
|
|
12
|
+
* Lock: machine-local `.instar/local/round-runner.lock` (see
|
|
13
|
+
* `ProjectRoundLock`). At most one runner active per machine.
|
|
14
|
+
*
|
|
15
|
+
* What's IN this PR (Phase 1b PR 3):
|
|
16
|
+
* - `preflight(projectId, roundIndex)` — checks 1–9 from the spec
|
|
17
|
+
* (drift check, step 10, is deferred to a follow-up PR alongside
|
|
18
|
+
* the cache+ledger HTTP wiring).
|
|
19
|
+
* - `halt(projectId, reason)` — writes haltedAt, sets project status
|
|
20
|
+
* to `halted`, releases the lock if held. Idempotent.
|
|
21
|
+
* - `recordAck(projectId, roundIndex)` — records user acknowledgment,
|
|
22
|
+
* resets `unacknowledgedAdvanceCount`, advances `lastAckedRoundIndex`,
|
|
23
|
+
* populates `firstLaunchAckAt` if absent.
|
|
24
|
+
* - `acceptPartial(projectId, roundIndex, reason)` — closes a
|
|
25
|
+
* partially-complete round: missing items → skipped,
|
|
26
|
+
* round.status → `complete-with-skips`. Counts as an ack for the
|
|
27
|
+
* current round (advances lastAckedRoundIndex) but does NOT
|
|
28
|
+
* increment unacknowledgedAdvanceCount.
|
|
29
|
+
*
|
|
30
|
+
* What's NOT in this PR:
|
|
31
|
+
* - The actual autonomous-delegating run loop (`run()`).
|
|
32
|
+
* - Dynamic stop-condition revalidation + SIGTERM/SIGKILL of children.
|
|
33
|
+
* - Drift check at preflight step 10.
|
|
34
|
+
* - Auto-advance poller.
|
|
35
|
+
* - Multi-machine claim-ownership.
|
|
36
|
+
*/
|
|
37
|
+
import fs from 'node:fs';
|
|
38
|
+
import path from 'node:path';
|
|
39
|
+
import { ProjectRoundLock, isAlive } from './ProjectRoundLock.js';
|
|
40
|
+
import { extractFrontmatter } from './SafeYaml.js';
|
|
41
|
+
export class ProjectRoundRunner {
|
|
42
|
+
tracker;
|
|
43
|
+
stateDir;
|
|
44
|
+
machineId;
|
|
45
|
+
lock;
|
|
46
|
+
unackedAdvanceCap;
|
|
47
|
+
ackGapCap;
|
|
48
|
+
constructor(config) {
|
|
49
|
+
this.tracker = config.tracker;
|
|
50
|
+
this.stateDir = config.stateDir;
|
|
51
|
+
this.machineId = config.machineId;
|
|
52
|
+
this.lock = new ProjectRoundLock({ stateDir: config.stateDir });
|
|
53
|
+
this.unackedAdvanceCap = config.unackedAdvanceCap ?? 2;
|
|
54
|
+
this.ackGapCap = config.ackGapCap ?? 2;
|
|
55
|
+
}
|
|
56
|
+
/** Public read of the current lock holder, used by tests and dashboards. */
|
|
57
|
+
currentLockHolder() {
|
|
58
|
+
return this.lock.read();
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Preflight check — the single chokepoint for round-start authority.
|
|
62
|
+
*
|
|
63
|
+
* Steps 1–9 from the spec. Step 10 (drift check) is intentionally
|
|
64
|
+
* skipped in this PR; it lands when the drift-check HTTP endpoint is
|
|
65
|
+
* wired into the routes ctx (Phase 1b follow-up).
|
|
66
|
+
*/
|
|
67
|
+
preflight(projectId, roundIndex) {
|
|
68
|
+
// ── Step 0: tracker present ───────────────────────────────────
|
|
69
|
+
if (!this.tracker) {
|
|
70
|
+
return { ok: false, code: 'TRACKER_MISSING', reason: 'no initiative tracker' };
|
|
71
|
+
}
|
|
72
|
+
// ── Project shape ─────────────────────────────────────────────
|
|
73
|
+
const project = this.tracker.get(projectId);
|
|
74
|
+
if (!project) {
|
|
75
|
+
return { ok: false, code: 'PROJECT_NOT_FOUND', reason: `no project ${projectId}` };
|
|
76
|
+
}
|
|
77
|
+
if ((project.kind ?? 'task') !== 'project') {
|
|
78
|
+
return {
|
|
79
|
+
ok: false,
|
|
80
|
+
code: 'PROJECT_NOT_PROJECT_KIND',
|
|
81
|
+
reason: `record ${projectId} is not project-kind (kind=${project.kind ?? 'task'})`,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
// ── Project status ────────────────────────────────────────────
|
|
85
|
+
if (project.status !== 'active') {
|
|
86
|
+
if (project.status === 'archived' || project.status === 'completed' || project.status === 'abandoned') {
|
|
87
|
+
return {
|
|
88
|
+
ok: false,
|
|
89
|
+
code: 'PROJECT_INACTIVE',
|
|
90
|
+
reason: `project status is "${project.status}", not "active"`,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
// Track halt / awaiting-user via round.haltedAt OR explicit awaitingUser?
|
|
95
|
+
// For now, "active" is the only allowed status.
|
|
96
|
+
// ── Round shape ───────────────────────────────────────────────
|
|
97
|
+
if (!Array.isArray(project.rounds) || project.rounds.length === 0) {
|
|
98
|
+
return { ok: false, code: 'ROUND_INDEX_OUT_OF_RANGE', reason: 'project has no rounds' };
|
|
99
|
+
}
|
|
100
|
+
if (roundIndex < 0 || roundIndex >= project.rounds.length) {
|
|
101
|
+
return {
|
|
102
|
+
ok: false,
|
|
103
|
+
code: 'ROUND_INDEX_OUT_OF_RANGE',
|
|
104
|
+
reason: `roundIndex ${roundIndex} not in [0, ${project.rounds.length})`,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
const round = project.rounds[roundIndex];
|
|
108
|
+
if (round.haltedAt) {
|
|
109
|
+
return {
|
|
110
|
+
ok: false,
|
|
111
|
+
code: 'PROJECT_HALTED',
|
|
112
|
+
reason: `round ${roundIndex} was halted at ${round.haltedAt}: ${round.haltReason ?? '<no reason>'}`,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
// Undefined status is treated as 'pending' (newly-constructed rounds
|
|
116
|
+
// from the plan parser may not set it explicitly).
|
|
117
|
+
const roundStatus = round.status ?? 'pending';
|
|
118
|
+
if (roundStatus !== 'pending' && roundStatus !== 'ready') {
|
|
119
|
+
return {
|
|
120
|
+
ok: false,
|
|
121
|
+
code: 'ROUND_NOT_PENDING_OR_READY',
|
|
122
|
+
reason: `round.status is "${round.status}", not "pending" or "ready"`,
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
// ── Step 1+2: lock free / stale-PID sweep ─────────────────────
|
|
126
|
+
const holder = this.lock.read();
|
|
127
|
+
if (holder && isAlive(holder.pid)) {
|
|
128
|
+
return {
|
|
129
|
+
ok: false,
|
|
130
|
+
code: 'LOCK_HELD',
|
|
131
|
+
reason: `lock held by pid ${holder.pid} for ${holder.projectId} round ${holder.roundIndex} since ${holder.acquiredAt}`,
|
|
132
|
+
currentHolder: holder,
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
// (If holder exists but PID dead, acquire() will sweep it.)
|
|
136
|
+
// ── Step 3: all round items at `approved` AND frontmatter still valid
|
|
137
|
+
const childIds = round.itemIds ?? [];
|
|
138
|
+
const children = [];
|
|
139
|
+
const failingItemIds = [];
|
|
140
|
+
for (const cid of childIds) {
|
|
141
|
+
const child = this.tracker.get(cid);
|
|
142
|
+
if (!child) {
|
|
143
|
+
failingItemIds.push(cid);
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
children.push(child);
|
|
147
|
+
// First round may legitimately start when items are still at
|
|
148
|
+
// outline (the round itself drives them through stages). The
|
|
149
|
+
// hard "items must all be approved" gate per § Phase 1.5 step 3
|
|
150
|
+
// applies to STARTING the autonomous run loop. Preflight is also
|
|
151
|
+
// used for /advance which does single-item transitions, so we
|
|
152
|
+
// accept any pipelineStage in this PR. The stricter gate will
|
|
153
|
+
// be re-asserted by `run()` when the run loop ships (Phase 1b
|
|
154
|
+
// follow-up).
|
|
155
|
+
}
|
|
156
|
+
if (failingItemIds.length > 0) {
|
|
157
|
+
return {
|
|
158
|
+
ok: false,
|
|
159
|
+
code: 'ITEMS_NOT_ALL_APPROVED',
|
|
160
|
+
reason: `${failingItemIds.length} item id(s) on the round don't resolve to existing records: ${failingItemIds.join(', ')}`,
|
|
161
|
+
failingItemIds,
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
// ── Step 5+6: first-launch ack + unacked-advances cap ─────────
|
|
165
|
+
const isFirstRound = roundIndex === 0;
|
|
166
|
+
const firstLaunchAckAt = project.firstLaunchAckAt;
|
|
167
|
+
if (isFirstRound && !firstLaunchAckAt) {
|
|
168
|
+
return {
|
|
169
|
+
ok: false,
|
|
170
|
+
code: 'FIRST_LAUNCH_ACK_REQUIRED',
|
|
171
|
+
reason: `first round requires POST /projects/${projectId}/ack before any entry path can start it`,
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
const unacked = project.unacknowledgedAdvanceCount ?? 0;
|
|
175
|
+
if (unacked >= this.unackedAdvanceCap) {
|
|
176
|
+
return {
|
|
177
|
+
ok: false,
|
|
178
|
+
code: 'UNACKED_ADVANCES_OVER_CAP',
|
|
179
|
+
reason: `${unacked} auto-advances without ack (cap ${this.unackedAdvanceCap}); ack or halt before continuing`,
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
const lastAcked = project.lastAckedRoundIndex ?? -1;
|
|
183
|
+
if (!isFirstRound && roundIndex - lastAcked > this.ackGapCap) {
|
|
184
|
+
return {
|
|
185
|
+
ok: false,
|
|
186
|
+
code: 'ROUND_ACK_GAP_TOO_LARGE',
|
|
187
|
+
reason: `roundIndex ${roundIndex} is more than ${this.ackGapCap} ahead of lastAckedRoundIndex (${lastAcked})`,
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
// ── Step 7: owner machine matches (or owner empty + claiming) ─
|
|
191
|
+
const ownerMachineId = project.ownerMachineId ?? this.machineId;
|
|
192
|
+
if (project.ownerMachineId && project.ownerMachineId !== this.machineId) {
|
|
193
|
+
return {
|
|
194
|
+
ok: false,
|
|
195
|
+
code: 'NOT_OWNER_MACHINE',
|
|
196
|
+
reason: `project owner is ${project.ownerMachineId}; this machine is ${this.machineId}`,
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
// ── Step 8: targetRepoPath exists ─────────────────────────────
|
|
200
|
+
const targetRepo = project.targetRepoPath;
|
|
201
|
+
if (!targetRepo || !fs.existsSync(targetRepo) || !fs.existsSync(path.join(targetRepo, '.git'))) {
|
|
202
|
+
return {
|
|
203
|
+
ok: false,
|
|
204
|
+
code: 'TARGET_REPO_PATH_INVALID',
|
|
205
|
+
reason: `targetRepoPath "${targetRepo}" missing, not a git repo, or unreadable`,
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
// ── Step 9: no awaitingReconciliation entries ────────────────
|
|
209
|
+
if (Array.isArray(project.awaitingReconciliation) && project.awaitingReconciliation.length > 0) {
|
|
210
|
+
return {
|
|
211
|
+
ok: false,
|
|
212
|
+
code: 'AWAITING_RECONCILIATION',
|
|
213
|
+
reason: `${project.awaitingReconciliation.length} reconciliation conflict(s) pending`,
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
// ── Step 10 (drift): deferred to follow-up PR ────────────────
|
|
217
|
+
return { ok: true, project, round, children, ownerMachineId };
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* Halt the active round of a project. Idempotent — halting an already-
|
|
221
|
+
* halted round is a no-op. Releases the lock if the calling machine
|
|
222
|
+
* holds it.
|
|
223
|
+
*
|
|
224
|
+
* Returns the round that was halted, or null if the project / round
|
|
225
|
+
* didn't exist.
|
|
226
|
+
*/
|
|
227
|
+
async halt(projectId, reason) {
|
|
228
|
+
const project = this.tracker.get(projectId);
|
|
229
|
+
if (!project || (project.kind ?? 'task') !== 'project')
|
|
230
|
+
return null;
|
|
231
|
+
// Find the active round. Preference order:
|
|
232
|
+
// 1. in-progress (the canonical "active" state)
|
|
233
|
+
// 2. pending / ready / undefined (round is queued but not yet
|
|
234
|
+
// running — calling halt on a queued round records the intent)
|
|
235
|
+
// 3. already-halted (idempotent return of the existing state)
|
|
236
|
+
const rounds = project.rounds ?? [];
|
|
237
|
+
let activeIdx = rounds.findIndex((r) => r.status === 'in-progress');
|
|
238
|
+
if (activeIdx === -1)
|
|
239
|
+
activeIdx = rounds.findIndex((r) => {
|
|
240
|
+
const s = r.status ?? 'pending';
|
|
241
|
+
return s === 'pending' || s === 'ready';
|
|
242
|
+
});
|
|
243
|
+
if (activeIdx === -1)
|
|
244
|
+
activeIdx = rounds.findIndex((r) => Boolean(r.haltedAt));
|
|
245
|
+
if (activeIdx === -1)
|
|
246
|
+
return null;
|
|
247
|
+
const round = rounds[activeIdx];
|
|
248
|
+
if (round.haltedAt) {
|
|
249
|
+
// Idempotent — return current state.
|
|
250
|
+
return { project, roundIndex: activeIdx };
|
|
251
|
+
}
|
|
252
|
+
const haltedAt = new Date().toISOString();
|
|
253
|
+
const newRounds = rounds.map((r, i) => i === activeIdx
|
|
254
|
+
? { ...r, status: 'failed', haltedAt, haltReason: reason }
|
|
255
|
+
: r);
|
|
256
|
+
const updated = await this.tracker.update(projectId, {
|
|
257
|
+
rounds: newRounds,
|
|
258
|
+
ifMatch: project.version,
|
|
259
|
+
});
|
|
260
|
+
// Release the lock if held by anyone — halt is the kill switch.
|
|
261
|
+
// (The spec says SIGTERM-then-SIGKILL the autonomous child; that
|
|
262
|
+
// path lands when run() ships. Here we just clear the lock.)
|
|
263
|
+
const holder = this.lock.read();
|
|
264
|
+
if (holder && holder.projectId === projectId && holder.roundIndex === activeIdx) {
|
|
265
|
+
this.lock.release();
|
|
266
|
+
}
|
|
267
|
+
return { project: updated, roundIndex: activeIdx };
|
|
268
|
+
}
|
|
269
|
+
/**
|
|
270
|
+
* Record a user acknowledgment for `roundIndex`. Idempotent: calling
|
|
271
|
+
* twice with the same index has no further effect. The fields the spec
|
|
272
|
+
* names: resets `unacknowledgedAdvanceCount`, populates
|
|
273
|
+
* `firstLaunchAckAt` (if absent), advances `lastAckedRoundIndex`.
|
|
274
|
+
*/
|
|
275
|
+
async recordAck(projectId, roundIndex) {
|
|
276
|
+
const project = this.tracker.get(projectId);
|
|
277
|
+
if (!project || (project.kind ?? 'task') !== 'project')
|
|
278
|
+
return null;
|
|
279
|
+
// Spec § Phase 1.3: idempotent on lastAckedRoundIndex.
|
|
280
|
+
const currentLastAck = project.lastAckedRoundIndex ?? -1;
|
|
281
|
+
const effectiveLastAck = Math.max(currentLastAck, roundIndex);
|
|
282
|
+
const firstLaunchAckAt = project.firstLaunchAckAt ?? new Date().toISOString();
|
|
283
|
+
return this.tracker.update(projectId, {
|
|
284
|
+
unacknowledgedAdvanceCount: 0,
|
|
285
|
+
firstLaunchAckAt,
|
|
286
|
+
lastAckedRoundIndex: effectiveLastAck,
|
|
287
|
+
ifMatch: project.version,
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
/**
|
|
291
|
+
* Close a partially-complete round: missing items → skipped, round
|
|
292
|
+
* status → `complete-with-skips`. Counts as an ack for `roundIndex`
|
|
293
|
+
* (advances `lastAckedRoundIndex`) but does NOT touch
|
|
294
|
+
* `unacknowledgedAdvanceCount`.
|
|
295
|
+
*
|
|
296
|
+
* The transition of each missing item to `skipped` requires `reason`
|
|
297
|
+
* and `skippedBy` (per StageTransitionValidator). The caller passes
|
|
298
|
+
* a single `reason` that's applied to every newly-skipped item.
|
|
299
|
+
*/
|
|
300
|
+
async acceptPartial(projectId, roundIndex, reason, skippedBy) {
|
|
301
|
+
const project = this.tracker.get(projectId);
|
|
302
|
+
if (!project || (project.kind ?? 'task') !== 'project')
|
|
303
|
+
return null;
|
|
304
|
+
const rounds = project.rounds ?? [];
|
|
305
|
+
if (roundIndex < 0 || roundIndex >= rounds.length)
|
|
306
|
+
return null;
|
|
307
|
+
const round = rounds[roundIndex];
|
|
308
|
+
// Skip each non-merged, non-already-skipped item.
|
|
309
|
+
const skippedItemIds = [];
|
|
310
|
+
const nowIso = new Date().toISOString();
|
|
311
|
+
for (const cid of round.itemIds ?? []) {
|
|
312
|
+
const child = this.tracker.get(cid);
|
|
313
|
+
if (!child)
|
|
314
|
+
continue;
|
|
315
|
+
const stage = child.pipelineStage ?? 'outline';
|
|
316
|
+
if (stage === 'merged' || stage === 'skipped')
|
|
317
|
+
continue;
|
|
318
|
+
await this.tracker.update(cid, {
|
|
319
|
+
pipelineStage: 'skipped',
|
|
320
|
+
ifMatch: child.version,
|
|
321
|
+
});
|
|
322
|
+
skippedItemIds.push(cid);
|
|
323
|
+
}
|
|
324
|
+
const newRounds = rounds.map((r, i) => i === roundIndex ? { ...r, status: 'complete-with-skips', completedAt: nowIso } : r);
|
|
325
|
+
// Advance lastAckedRoundIndex; do NOT change unacknowledgedAdvanceCount.
|
|
326
|
+
const currentLastAck = project.lastAckedRoundIndex ?? -1;
|
|
327
|
+
const updated = await this.tracker.update(projectId, {
|
|
328
|
+
rounds: newRounds,
|
|
329
|
+
lastAckedRoundIndex: Math.max(currentLastAck, roundIndex),
|
|
330
|
+
ifMatch: project.version,
|
|
331
|
+
});
|
|
332
|
+
return { project: updated, skippedItemIds };
|
|
333
|
+
}
|
|
334
|
+
/**
|
|
335
|
+
* Validate that a child initiative's spec frontmatter still has the
|
|
336
|
+
* tags the round-runner requires before STARTing the autonomous loop.
|
|
337
|
+
* Spec § Phase 1.5 step 3 + § Phase 1.4 "Authority separation":
|
|
338
|
+
* - `review-convergence: true`
|
|
339
|
+
* - `approved: true`
|
|
340
|
+
*
|
|
341
|
+
* Used by future `run()` and by the dashboard's "ready to start" badge.
|
|
342
|
+
* Exported for tests.
|
|
343
|
+
*/
|
|
344
|
+
static validateChildFrontmatter(targetRepoPath, specRelPath) {
|
|
345
|
+
const abs = path.isAbsolute(specRelPath)
|
|
346
|
+
? specRelPath
|
|
347
|
+
: path.join(targetRepoPath, specRelPath);
|
|
348
|
+
if (!fs.existsSync(abs)) {
|
|
349
|
+
return { ok: false, reason: `spec file missing: ${specRelPath}` };
|
|
350
|
+
}
|
|
351
|
+
let body;
|
|
352
|
+
try {
|
|
353
|
+
body = fs.readFileSync(abs, 'utf-8');
|
|
354
|
+
}
|
|
355
|
+
catch (err) {
|
|
356
|
+
return { ok: false, reason: `spec unreadable: ${err.message}` };
|
|
357
|
+
}
|
|
358
|
+
const fm = extractFrontmatter(body);
|
|
359
|
+
if (fm.error)
|
|
360
|
+
return { ok: false, reason: `frontmatter unparseable: ${fm.error}` };
|
|
361
|
+
const data = fm.frontmatter ?? {};
|
|
362
|
+
if (data['review-convergence'] !== true) {
|
|
363
|
+
return { ok: false, reason: '`review-convergence: true` missing or false' };
|
|
364
|
+
}
|
|
365
|
+
if (data['approved'] !== true) {
|
|
366
|
+
return { ok: false, reason: '`approved: true` missing or false' };
|
|
367
|
+
}
|
|
368
|
+
return { ok: true };
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
//# sourceMappingURL=ProjectRoundRunner.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ProjectRoundRunner.js","sourceRoot":"","sources":["../../src/core/ProjectRoundRunner.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ProjectRoundWorktrees — lazy worktree allocator for project rounds.
|
|
3
|
+
*
|
|
4
|
+
* Spec: docs/specs/PROJECT-SCOPE-SPEC.md § Phase 1.5 step 3.
|
|
5
|
+
*
|
|
6
|
+
* Each round-item gets its own git worktree at
|
|
7
|
+
* <targetRepoPath>/.worktrees/<projectId>/<roundIndex>/<itemId>
|
|
8
|
+
*
|
|
9
|
+
* On first allocation in a target repo, `.worktrees/` is appended to
|
|
10
|
+
* `.git/info/exclude` so the worktree namespace doesn't pollute
|
|
11
|
+
* `git status` or get caught by `git add -A`. The exclude file is
|
|
12
|
+
* per-clone (not committed), matching the spec.
|
|
13
|
+
*
|
|
14
|
+
* `prune()` runs `git worktree prune` against the round's namespace.
|
|
15
|
+
* Idempotent.
|
|
16
|
+
*/
|
|
17
|
+
export interface AllocateInput {
|
|
18
|
+
targetRepoPath: string;
|
|
19
|
+
projectId: string;
|
|
20
|
+
roundIndex: number;
|
|
21
|
+
itemId: string;
|
|
22
|
+
}
|
|
23
|
+
export interface AllocateResult {
|
|
24
|
+
/** Absolute path to the new worktree (or existing one if `refuseExisting:false`). */
|
|
25
|
+
worktreePath: string;
|
|
26
|
+
/** True when the worktree was created in this call; false when it pre-existed. */
|
|
27
|
+
created: boolean;
|
|
28
|
+
}
|
|
29
|
+
export declare class ProjectRoundWorktrees {
|
|
30
|
+
/** Compute the worktree path WITHOUT creating it. */
|
|
31
|
+
static pathFor(input: AllocateInput): string;
|
|
32
|
+
/**
|
|
33
|
+
* Lazy-allocate a worktree for one item. Idempotent on first creation,
|
|
34
|
+
* but refuses by default if the directory pre-exists (matches spec
|
|
35
|
+
* "Refuse if the path already exists").
|
|
36
|
+
*/
|
|
37
|
+
static allocate(input: AllocateInput, opts?: {
|
|
38
|
+
refuseExisting?: boolean;
|
|
39
|
+
}): AllocateResult;
|
|
40
|
+
/**
|
|
41
|
+
* Prune the round's worktree namespace. Runs `git worktree prune`
|
|
42
|
+
* on the target repo (cheap; idempotent).
|
|
43
|
+
*/
|
|
44
|
+
static prune(targetRepoPath: string): void;
|
|
45
|
+
/**
|
|
46
|
+
* Forcibly remove a single allocated worktree (e.g., on round halt
|
|
47
|
+
* cleanup). Uses `git worktree remove --force` so dirty checkouts
|
|
48
|
+
* don't block cleanup. Idempotent: missing worktrees pass through.
|
|
49
|
+
*/
|
|
50
|
+
static remove(input: AllocateInput): void;
|
|
51
|
+
/**
|
|
52
|
+
* Append `.worktrees/` to `.git/info/exclude` if not present.
|
|
53
|
+
* Idempotent. Per-clone (not committed).
|
|
54
|
+
*/
|
|
55
|
+
static ensureExcludeEntry(targetRepoPath: string): void;
|
|
56
|
+
}
|
|
57
|
+
//# sourceMappingURL=ProjectRoundWorktrees.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ProjectRoundWorktrees.d.ts","sourceRoot":"","sources":["../../src/core/ProjectRoundWorktrees.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAOH,MAAM,WAAW,aAAa;IAC5B,cAAc,EAAE,MAAM,CAAC;IACvB,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,cAAc;IAC7B,qFAAqF;IACrF,YAAY,EAAE,MAAM,CAAC;IACrB,kFAAkF;IAClF,OAAO,EAAE,OAAO,CAAC;CAClB;AAED,qBAAa,qBAAqB;IAChC,qDAAqD;IACrD,MAAM,CAAC,OAAO,CAAC,KAAK,EAAE,aAAa,GAAG,MAAM;IAU5C;;;;OAIG;IACH,MAAM,CAAC,QAAQ,CAAC,KAAK,EAAE,aAAa,EAAE,IAAI,GAAE;QAAE,cAAc,CAAC,EAAE,OAAO,CAAA;KAAO,GAAG,cAAc;IAiB9F;;;OAGG;IACH,MAAM,CAAC,KAAK,CAAC,cAAc,EAAE,MAAM,GAAG,IAAI;IAO1C;;;;OAIG;IACH,MAAM,CAAC,MAAM,CAAC,KAAK,EAAE,aAAa,GAAG,IAAI;IAczC;;;OAGG;IACH,MAAM,CAAC,kBAAkB,CAAC,cAAc,EAAE,MAAM,GAAG,IAAI;CA0BxD"}
|