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,158 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ProjectRoundCompleteMessage — tone-gated round-complete delivery
|
|
3
|
+
* template + retry + idempotency for project rounds.
|
|
4
|
+
*
|
|
5
|
+
* Spec: docs/specs/PROJECT-SCOPE-SPEC.md § Phase 1.8.
|
|
6
|
+
*
|
|
7
|
+
* Two responsibilities:
|
|
8
|
+
* 1. `formatRoundCompleteMessage(input)` — pure template function
|
|
9
|
+
* that validates required-field PRESENCE (never non-emptiness)
|
|
10
|
+
* and returns a `{message, idempotencyKey}` pair. Pre-flight
|
|
11
|
+
* halts have a documented empty-default for `whatLanded` so the
|
|
12
|
+
* gate never silently rejects a legitimate halt event.
|
|
13
|
+
* 2. `RoundCompleteDeliveryHelper` — bookkeeping over the
|
|
14
|
+
* idempotency key set with a `sendOnce` helper that delegates
|
|
15
|
+
* actual transport to a caller-provided sender, retries on
|
|
16
|
+
* transient failures with exponential backoff, and triggers the
|
|
17
|
+
* caller's fallback (`onPermanentFail`) when all retries fail.
|
|
18
|
+
*
|
|
19
|
+
* What this file is NOT responsible for:
|
|
20
|
+
* - Choosing WHEN to send a round-complete message. The autonomous
|
|
21
|
+
* run loop (next PR) decides that.
|
|
22
|
+
* - The actual Telegram transport. The caller passes a `send`
|
|
23
|
+
* function — keeping the template + retry isolated from the
|
|
24
|
+
* `TelegramAdapter` so tests don't need to spin up the adapter.
|
|
25
|
+
* - Going through the `MessagingToneGate`. The gate operates on
|
|
26
|
+
* final outbound text; the consumer is the one that calls
|
|
27
|
+
* `gate.evaluate(message)` before passing the message to a
|
|
28
|
+
* transport. We expose the formatted text so the consumer can
|
|
29
|
+
* route it through the gate.
|
|
30
|
+
*/
|
|
31
|
+
import { SafeFsExecutor } from './SafeFsExecutor.js';
|
|
32
|
+
/** Event kind the message describes. */
|
|
33
|
+
export type RoundCompleteEventKind = 'round-complete' | 'round-partially-complete' | 'round-halted' | 'round-failed';
|
|
34
|
+
/** Inputs to the template. Required-fields are enforced by the validator below. */
|
|
35
|
+
export interface RoundCompleteMessageInput {
|
|
36
|
+
projectId: string;
|
|
37
|
+
projectTitle: string;
|
|
38
|
+
roundIndex: number;
|
|
39
|
+
/** Bumped on every project record write — part of the idempotency key. */
|
|
40
|
+
projectVersion: number;
|
|
41
|
+
eventKind: RoundCompleteEventKind;
|
|
42
|
+
/**
|
|
43
|
+
* Bullet list of merged itemIds with titles. May default to
|
|
44
|
+
* "No items shipped this round — halted at <step>" when the event
|
|
45
|
+
* is a pre-flight halt; presence is enforced, non-emptiness is not.
|
|
46
|
+
*/
|
|
47
|
+
whatLanded: string;
|
|
48
|
+
/** Required for halt events (round-halted / round-failed). */
|
|
49
|
+
whatHalted?: string;
|
|
50
|
+
/**
|
|
51
|
+
* Verified citations (text excerpts the dashboard renders), or PR
|
|
52
|
+
* mergeCommit.oid values for shipped items. Empty array is allowed.
|
|
53
|
+
*/
|
|
54
|
+
evidenceCited: string[];
|
|
55
|
+
/**
|
|
56
|
+
* Required for halt + failed events. For clean `round-complete`,
|
|
57
|
+
* the default `"(none)"` is accepted by the template.
|
|
58
|
+
*/
|
|
59
|
+
rootCauseHypothesis: string;
|
|
60
|
+
/**
|
|
61
|
+
* Required. Tells the user how to act:
|
|
62
|
+
* `"Reply 'pause <project-id>' within 24 hours to hold"` is the
|
|
63
|
+
* canonical phrasing per spec.
|
|
64
|
+
*/
|
|
65
|
+
concreteNextStep: string;
|
|
66
|
+
/** Optional dashboard deep link. */
|
|
67
|
+
overrideLink?: string;
|
|
68
|
+
/** Required. Canonical phrasing for the user's hold path. */
|
|
69
|
+
brakeHandlePhrase: string;
|
|
70
|
+
}
|
|
71
|
+
/** Stable key keyed off `(projectId, roundIndex, eventKind, projectVersion)`. */
|
|
72
|
+
export declare function idempotencyKeyFor(input: Pick<RoundCompleteMessageInput, 'projectId' | 'roundIndex' | 'eventKind' | 'projectVersion'>): string;
|
|
73
|
+
export type FormatResult = {
|
|
74
|
+
ok: true;
|
|
75
|
+
message: string;
|
|
76
|
+
idempotencyKey: string;
|
|
77
|
+
} | {
|
|
78
|
+
ok: false;
|
|
79
|
+
missingFields: string[];
|
|
80
|
+
};
|
|
81
|
+
/**
|
|
82
|
+
* Build the user-facing message. Returns `{ok:false, missingFields}`
|
|
83
|
+
* when a required field is undefined / null. Returns `{ok:true, message,
|
|
84
|
+
* idempotencyKey}` otherwise. Empty strings are accepted by design — the
|
|
85
|
+
* template enforces *presence*, not *non-emptiness*. The empty-default
|
|
86
|
+
* for `whatLanded` on pre-flight halts is the responsibility of the
|
|
87
|
+
* caller (they should supply the documented default before calling
|
|
88
|
+
* this function — the validator just records that the field is present).
|
|
89
|
+
*/
|
|
90
|
+
export declare function formatRoundCompleteMessage(input: RoundCompleteMessageInput): FormatResult;
|
|
91
|
+
export type SendResult = {
|
|
92
|
+
ok: true;
|
|
93
|
+
} | {
|
|
94
|
+
ok: false;
|
|
95
|
+
transient: boolean;
|
|
96
|
+
error: Error;
|
|
97
|
+
};
|
|
98
|
+
export interface SendAttempt {
|
|
99
|
+
attempt: number;
|
|
100
|
+
ok: boolean;
|
|
101
|
+
error?: Error;
|
|
102
|
+
transient?: boolean;
|
|
103
|
+
}
|
|
104
|
+
export interface DeliveryReport {
|
|
105
|
+
sent: boolean;
|
|
106
|
+
attempts: SendAttempt[];
|
|
107
|
+
/** Set when send succeeded — null on failure. */
|
|
108
|
+
idempotencyKey: string | null;
|
|
109
|
+
/** Set when ALL attempts failed AND the caller's fallback ran. */
|
|
110
|
+
fallbackTriggered: boolean;
|
|
111
|
+
}
|
|
112
|
+
export interface RoundCompleteDeliveryHelperConfig {
|
|
113
|
+
/** Absolute path to the agent's `.instar/` directory (for the dedup file). */
|
|
114
|
+
stateDir: string;
|
|
115
|
+
/** Max attempts before the fallback fires. Default 3. */
|
|
116
|
+
maxAttempts?: number;
|
|
117
|
+
/** Backoff base in ms. Default 1000 (1s, 2s, 4s). */
|
|
118
|
+
backoffBaseMs?: number;
|
|
119
|
+
/** Override for tests. */
|
|
120
|
+
setTimeoutFn?: (cb: () => void, ms: number) => void;
|
|
121
|
+
}
|
|
122
|
+
declare const DEDUP_FILE = "round-complete-sent.json";
|
|
123
|
+
export declare class RoundCompleteDeliveryHelper {
|
|
124
|
+
private stateDir;
|
|
125
|
+
private maxAttempts;
|
|
126
|
+
private backoffBaseMs;
|
|
127
|
+
private setTimeoutFn;
|
|
128
|
+
private sentKeys;
|
|
129
|
+
private loaded;
|
|
130
|
+
constructor(config: RoundCompleteDeliveryHelperConfig);
|
|
131
|
+
/**
|
|
132
|
+
* Send a round-complete message, with retry + idempotency.
|
|
133
|
+
*
|
|
134
|
+
* On the first call for an idempotency key:
|
|
135
|
+
* - Calls `send(input, message)` up to `maxAttempts` times.
|
|
136
|
+
* - Between attempts, waits `backoffBaseMs * 2^(attempt-1)` ms.
|
|
137
|
+
* - Records the key after the first successful send.
|
|
138
|
+
*
|
|
139
|
+
* On subsequent calls with the same key:
|
|
140
|
+
* - Returns `{sent: false, alreadySent: true}` without calling `send`.
|
|
141
|
+
*
|
|
142
|
+
* On permanent failure (all attempts return non-transient or transient
|
|
143
|
+
* exhausted), invokes `onPermanentFail(input, message, attempts)`.
|
|
144
|
+
*/
|
|
145
|
+
sendOnce(input: RoundCompleteMessageInput, send: (input: RoundCompleteMessageInput, message: string) => Promise<SendResult>, onPermanentFail?: (input: RoundCompleteMessageInput, message: string, attempts: SendAttempt[]) => Promise<void>): Promise<DeliveryReport & {
|
|
146
|
+
alreadySent?: boolean;
|
|
147
|
+
}>;
|
|
148
|
+
/** Test-only: query whether a key was recorded. */
|
|
149
|
+
hasSent(key: string): boolean;
|
|
150
|
+
private loadIfNeeded;
|
|
151
|
+
private persist;
|
|
152
|
+
private dedupPath;
|
|
153
|
+
}
|
|
154
|
+
/** Re-export so tests can clean up the dedup file directly. */
|
|
155
|
+
export { DEDUP_FILE };
|
|
156
|
+
/** Re-export SafeFsExecutor namespace for callers that need to clean up safely. */
|
|
157
|
+
export { SafeFsExecutor };
|
|
158
|
+
//# sourceMappingURL=ProjectRoundCompleteMessage.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ProjectRoundCompleteMessage.d.ts","sourceRoot":"","sources":["../../src/core/ProjectRoundCompleteMessage.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ProjectRoundCompleteMessage — tone-gated round-complete delivery
|
|
3
|
+
* template + retry + idempotency for project rounds.
|
|
4
|
+
*
|
|
5
|
+
* Spec: docs/specs/PROJECT-SCOPE-SPEC.md § Phase 1.8.
|
|
6
|
+
*
|
|
7
|
+
* Two responsibilities:
|
|
8
|
+
* 1. `formatRoundCompleteMessage(input)` — pure template function
|
|
9
|
+
* that validates required-field PRESENCE (never non-emptiness)
|
|
10
|
+
* and returns a `{message, idempotencyKey}` pair. Pre-flight
|
|
11
|
+
* halts have a documented empty-default for `whatLanded` so the
|
|
12
|
+
* gate never silently rejects a legitimate halt event.
|
|
13
|
+
* 2. `RoundCompleteDeliveryHelper` — bookkeeping over the
|
|
14
|
+
* idempotency key set with a `sendOnce` helper that delegates
|
|
15
|
+
* actual transport to a caller-provided sender, retries on
|
|
16
|
+
* transient failures with exponential backoff, and triggers the
|
|
17
|
+
* caller's fallback (`onPermanentFail`) when all retries fail.
|
|
18
|
+
*
|
|
19
|
+
* What this file is NOT responsible for:
|
|
20
|
+
* - Choosing WHEN to send a round-complete message. The autonomous
|
|
21
|
+
* run loop (next PR) decides that.
|
|
22
|
+
* - The actual Telegram transport. The caller passes a `send`
|
|
23
|
+
* function — keeping the template + retry isolated from the
|
|
24
|
+
* `TelegramAdapter` so tests don't need to spin up the adapter.
|
|
25
|
+
* - Going through the `MessagingToneGate`. The gate operates on
|
|
26
|
+
* final outbound text; the consumer is the one that calls
|
|
27
|
+
* `gate.evaluate(message)` before passing the message to a
|
|
28
|
+
* transport. We expose the formatted text so the consumer can
|
|
29
|
+
* route it through the gate.
|
|
30
|
+
*/
|
|
31
|
+
import fs from 'node:fs';
|
|
32
|
+
import path from 'node:path';
|
|
33
|
+
import { SafeFsExecutor } from './SafeFsExecutor.js';
|
|
34
|
+
/** Stable key keyed off `(projectId, roundIndex, eventKind, projectVersion)`. */
|
|
35
|
+
export function idempotencyKeyFor(input) {
|
|
36
|
+
return `${input.projectId}::${input.roundIndex}::${input.eventKind}::v${input.projectVersion}`;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Build the user-facing message. Returns `{ok:false, missingFields}`
|
|
40
|
+
* when a required field is undefined / null. Returns `{ok:true, message,
|
|
41
|
+
* idempotencyKey}` otherwise. Empty strings are accepted by design — the
|
|
42
|
+
* template enforces *presence*, not *non-emptiness*. The empty-default
|
|
43
|
+
* for `whatLanded` on pre-flight halts is the responsibility of the
|
|
44
|
+
* caller (they should supply the documented default before calling
|
|
45
|
+
* this function — the validator just records that the field is present).
|
|
46
|
+
*/
|
|
47
|
+
export function formatRoundCompleteMessage(input) {
|
|
48
|
+
const missing = [];
|
|
49
|
+
if (typeof input.projectId !== 'string')
|
|
50
|
+
missing.push('projectId');
|
|
51
|
+
if (typeof input.projectTitle !== 'string')
|
|
52
|
+
missing.push('projectTitle');
|
|
53
|
+
if (!Number.isInteger(input.roundIndex))
|
|
54
|
+
missing.push('roundIndex');
|
|
55
|
+
if (!Number.isInteger(input.projectVersion))
|
|
56
|
+
missing.push('projectVersion');
|
|
57
|
+
if (typeof input.eventKind !== 'string')
|
|
58
|
+
missing.push('eventKind');
|
|
59
|
+
if (typeof input.whatLanded !== 'string')
|
|
60
|
+
missing.push('whatLanded');
|
|
61
|
+
if (typeof input.rootCauseHypothesis !== 'string')
|
|
62
|
+
missing.push('rootCauseHypothesis');
|
|
63
|
+
if (typeof input.concreteNextStep !== 'string')
|
|
64
|
+
missing.push('concreteNextStep');
|
|
65
|
+
if (typeof input.brakeHandlePhrase !== 'string')
|
|
66
|
+
missing.push('brakeHandlePhrase');
|
|
67
|
+
if (!Array.isArray(input.evidenceCited))
|
|
68
|
+
missing.push('evidenceCited');
|
|
69
|
+
// halt-flavor events also require whatHalted to be present (string).
|
|
70
|
+
const haltLike = input.eventKind === 'round-halted' || input.eventKind === 'round-failed';
|
|
71
|
+
if (haltLike && typeof input.whatHalted !== 'string')
|
|
72
|
+
missing.push('whatHalted');
|
|
73
|
+
if (missing.length > 0) {
|
|
74
|
+
return { ok: false, missingFields: missing };
|
|
75
|
+
}
|
|
76
|
+
const lines = [];
|
|
77
|
+
const headlineByKind = {
|
|
78
|
+
'round-complete': 'Round complete',
|
|
79
|
+
'round-partially-complete': 'Round partially complete',
|
|
80
|
+
'round-halted': 'Round halted',
|
|
81
|
+
'round-failed': 'Round failed',
|
|
82
|
+
};
|
|
83
|
+
lines.push(`${headlineByKind[input.eventKind]}: ${input.projectTitle} (round ${input.roundIndex}).`);
|
|
84
|
+
lines.push('');
|
|
85
|
+
lines.push('What landed:');
|
|
86
|
+
lines.push(input.whatLanded);
|
|
87
|
+
if (haltLike && input.whatHalted) {
|
|
88
|
+
lines.push('');
|
|
89
|
+
lines.push('What halted:');
|
|
90
|
+
lines.push(input.whatHalted);
|
|
91
|
+
}
|
|
92
|
+
if (input.evidenceCited.length > 0) {
|
|
93
|
+
lines.push('');
|
|
94
|
+
lines.push('Evidence cited:');
|
|
95
|
+
for (const e of input.evidenceCited.slice(0, 10))
|
|
96
|
+
lines.push(`• ${e}`);
|
|
97
|
+
}
|
|
98
|
+
if (input.rootCauseHypothesis && input.rootCauseHypothesis !== '(none)') {
|
|
99
|
+
lines.push('');
|
|
100
|
+
lines.push(`Root cause hypothesis: ${input.rootCauseHypothesis}`);
|
|
101
|
+
}
|
|
102
|
+
lines.push('');
|
|
103
|
+
lines.push(`Next step: ${input.concreteNextStep}`);
|
|
104
|
+
lines.push(`To hold: ${input.brakeHandlePhrase}`);
|
|
105
|
+
if (input.overrideLink) {
|
|
106
|
+
lines.push(`Dashboard: ${input.overrideLink}`);
|
|
107
|
+
}
|
|
108
|
+
return {
|
|
109
|
+
ok: true,
|
|
110
|
+
message: lines.join('\n'),
|
|
111
|
+
idempotencyKey: idempotencyKeyFor(input),
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
const DEDUP_FILE = 'round-complete-sent.json';
|
|
115
|
+
/** Bounded set so dedupe state doesn't grow forever. */
|
|
116
|
+
const DEDUP_RING_SIZE = 1000;
|
|
117
|
+
export class RoundCompleteDeliveryHelper {
|
|
118
|
+
stateDir;
|
|
119
|
+
maxAttempts;
|
|
120
|
+
backoffBaseMs;
|
|
121
|
+
setTimeoutFn;
|
|
122
|
+
sentKeys = new Set();
|
|
123
|
+
loaded = false;
|
|
124
|
+
constructor(config) {
|
|
125
|
+
this.stateDir = config.stateDir;
|
|
126
|
+
this.maxAttempts = config.maxAttempts ?? 3;
|
|
127
|
+
this.backoffBaseMs = config.backoffBaseMs ?? 1000;
|
|
128
|
+
this.setTimeoutFn = config.setTimeoutFn ?? ((cb, ms) => setTimeout(cb, ms).unref());
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Send a round-complete message, with retry + idempotency.
|
|
132
|
+
*
|
|
133
|
+
* On the first call for an idempotency key:
|
|
134
|
+
* - Calls `send(input, message)` up to `maxAttempts` times.
|
|
135
|
+
* - Between attempts, waits `backoffBaseMs * 2^(attempt-1)` ms.
|
|
136
|
+
* - Records the key after the first successful send.
|
|
137
|
+
*
|
|
138
|
+
* On subsequent calls with the same key:
|
|
139
|
+
* - Returns `{sent: false, alreadySent: true}` without calling `send`.
|
|
140
|
+
*
|
|
141
|
+
* On permanent failure (all attempts return non-transient or transient
|
|
142
|
+
* exhausted), invokes `onPermanentFail(input, message, attempts)`.
|
|
143
|
+
*/
|
|
144
|
+
async sendOnce(input, send, onPermanentFail) {
|
|
145
|
+
this.loadIfNeeded();
|
|
146
|
+
const formatted = formatRoundCompleteMessage(input);
|
|
147
|
+
if (!formatted.ok) {
|
|
148
|
+
return { sent: false, attempts: [], idempotencyKey: null, fallbackTriggered: false };
|
|
149
|
+
}
|
|
150
|
+
if (this.sentKeys.has(formatted.idempotencyKey)) {
|
|
151
|
+
return {
|
|
152
|
+
sent: true,
|
|
153
|
+
attempts: [{ attempt: 0, ok: true }],
|
|
154
|
+
idempotencyKey: formatted.idempotencyKey,
|
|
155
|
+
fallbackTriggered: false,
|
|
156
|
+
alreadySent: true,
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
const attempts = [];
|
|
160
|
+
for (let i = 1; i <= this.maxAttempts; i++) {
|
|
161
|
+
let result;
|
|
162
|
+
try {
|
|
163
|
+
result = await send(input, formatted.message);
|
|
164
|
+
}
|
|
165
|
+
catch (err) {
|
|
166
|
+
result = { ok: false, transient: true, error: err instanceof Error ? err : new Error(String(err)) };
|
|
167
|
+
}
|
|
168
|
+
if (result.ok) {
|
|
169
|
+
attempts.push({ attempt: i, ok: true });
|
|
170
|
+
this.sentKeys.add(formatted.idempotencyKey);
|
|
171
|
+
this.persist();
|
|
172
|
+
return {
|
|
173
|
+
sent: true,
|
|
174
|
+
attempts,
|
|
175
|
+
idempotencyKey: formatted.idempotencyKey,
|
|
176
|
+
fallbackTriggered: false,
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
attempts.push({ attempt: i, ok: false, error: result.error, transient: result.transient });
|
|
180
|
+
// Non-transient → bail immediately.
|
|
181
|
+
if (!result.transient)
|
|
182
|
+
break;
|
|
183
|
+
// Transient → backoff before next attempt, unless this was the last.
|
|
184
|
+
if (i < this.maxAttempts) {
|
|
185
|
+
const delay = this.backoffBaseMs * Math.pow(2, i - 1);
|
|
186
|
+
await new Promise((resolve) => this.setTimeoutFn(resolve, delay));
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
if (onPermanentFail) {
|
|
190
|
+
try {
|
|
191
|
+
await onPermanentFail(input, formatted.message, attempts);
|
|
192
|
+
}
|
|
193
|
+
catch {
|
|
194
|
+
// Fallback's own failure is logged at the call site; we don't
|
|
195
|
+
// double-fail the delivery report.
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
return {
|
|
199
|
+
sent: false,
|
|
200
|
+
attempts,
|
|
201
|
+
idempotencyKey: null,
|
|
202
|
+
fallbackTriggered: !!onPermanentFail,
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
/** Test-only: query whether a key was recorded. */
|
|
206
|
+
hasSent(key) {
|
|
207
|
+
this.loadIfNeeded();
|
|
208
|
+
return this.sentKeys.has(key);
|
|
209
|
+
}
|
|
210
|
+
// ── Internals ─────────────────────────────────────────────────────
|
|
211
|
+
loadIfNeeded() {
|
|
212
|
+
if (this.loaded)
|
|
213
|
+
return;
|
|
214
|
+
this.loaded = true;
|
|
215
|
+
try {
|
|
216
|
+
const raw = fs.readFileSync(this.dedupPath(), 'utf-8');
|
|
217
|
+
const obj = JSON.parse(raw);
|
|
218
|
+
if (obj && Array.isArray(obj.sent)) {
|
|
219
|
+
for (const k of obj.sent)
|
|
220
|
+
if (typeof k === 'string')
|
|
221
|
+
this.sentKeys.add(k);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
catch {
|
|
225
|
+
// Missing or malformed dedup file — start fresh.
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
persist() {
|
|
229
|
+
try {
|
|
230
|
+
const dir = path.join(this.stateDir, 'local');
|
|
231
|
+
if (!fs.existsSync(dir))
|
|
232
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
233
|
+
// Cap the ring so the file doesn't grow forever.
|
|
234
|
+
const list = Array.from(this.sentKeys).slice(-DEDUP_RING_SIZE);
|
|
235
|
+
this.sentKeys = new Set(list);
|
|
236
|
+
const tmp = this.dedupPath() + '.tmp';
|
|
237
|
+
fs.writeFileSync(tmp, JSON.stringify({ sent: list }, null, 2), { mode: 0o600 });
|
|
238
|
+
fs.renameSync(tmp, this.dedupPath());
|
|
239
|
+
}
|
|
240
|
+
catch {
|
|
241
|
+
// Best-effort persistence; in-memory set still works for the lifetime
|
|
242
|
+
// of the process.
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
dedupPath() {
|
|
246
|
+
return path.join(this.stateDir, 'local', DEDUP_FILE);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
/** Re-export so tests can clean up the dedup file directly. */
|
|
250
|
+
export { DEDUP_FILE };
|
|
251
|
+
/** Re-export SafeFsExecutor namespace for callers that need to clean up safely. */
|
|
252
|
+
export { SafeFsExecutor };
|
|
253
|
+
//# sourceMappingURL=ProjectRoundCompleteMessage.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ProjectRoundCompleteMessage.js","sourceRoot":"","sources":["../../src/core/ProjectRoundCompleteMessage.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ProjectRoundExecution — the autonomous run loop for a project round.
|
|
3
|
+
*
|
|
4
|
+
* Spec: docs/specs/PROJECT-SCOPE-SPEC.md § Phase 1.5 ("Run loop", steps 1-11).
|
|
5
|
+
*
|
|
6
|
+
* What this module is responsible for:
|
|
7
|
+
* 1. Acquiring the round-runner lock (delegated to ProjectRoundLock).
|
|
8
|
+
* 2. Spawning the autonomous child process in a detached process
|
|
9
|
+
* group, so SIGTERM/SIGKILL of the group never reaps the runner.
|
|
10
|
+
* 3. Polling the project record every `pollIntervalMs` to detect
|
|
11
|
+
* mid-round mutations to the round's itemIds (e.g., a user
|
|
12
|
+
* manually skips an item) — when detected, SIGTERM the group
|
|
13
|
+
* (5s grace, then SIGKILL) and relaunch with a recomputed stop
|
|
14
|
+
* condition.
|
|
15
|
+
* 4. On the autonomous child's NATURAL exit, verify per-item
|
|
16
|
+
* artifacts and set round.status = complete | partially-complete.
|
|
17
|
+
* 5. Cleanup: `git worktree prune` for the round namespace.
|
|
18
|
+
* 6. Release the lock.
|
|
19
|
+
*
|
|
20
|
+
* What this module is NOT responsible for:
|
|
21
|
+
* - The preflight gate. That's `ProjectRoundRunner.preflight`. The
|
|
22
|
+
* caller (the auto-advance poller, the /project run-round skill,
|
|
23
|
+
* a future HTTP endpoint) is expected to preflight before calling
|
|
24
|
+
* `runRound(input)`.
|
|
25
|
+
* - Drift checking. Same — preflight handles it.
|
|
26
|
+
* - Choosing the autonomous command. The `spawnCommand` is injected,
|
|
27
|
+
* defaulting to `claude` (which invokes the local /autonomous skill).
|
|
28
|
+
* Tests pass a custom command so they don't depend on the skill.
|
|
29
|
+
*
|
|
30
|
+
* Process group safety:
|
|
31
|
+
* The child is spawned with `detached: true` so it gets its own
|
|
32
|
+
* process group. `kill(-pgid, signal)` (Node passes negative PIDs
|
|
33
|
+
* for group signals) targets only the child's group — never reaps
|
|
34
|
+
* the runner.
|
|
35
|
+
*/
|
|
36
|
+
import type { InitiativeTracker } from './InitiativeTracker.js';
|
|
37
|
+
/** Per-spec defaults. Tests dial both down to keep the suite fast. */
|
|
38
|
+
export declare const DEFAULT_POLL_INTERVAL_MS = 60000;
|
|
39
|
+
export declare const DEFAULT_SIGTERM_GRACE_MS = 5000;
|
|
40
|
+
/** Resume cap from spec § Phase 1.5 step 11. */
|
|
41
|
+
export declare const DEFAULT_MAX_RESUME_ATTEMPTS = 3;
|
|
42
|
+
export interface RunRoundInput {
|
|
43
|
+
tracker: InitiativeTracker;
|
|
44
|
+
projectId: string;
|
|
45
|
+
roundIndex: number;
|
|
46
|
+
/**
|
|
47
|
+
* Absolute target repo path; required (the spec's `targetRepoPath`).
|
|
48
|
+
* The caller must have verified it points at a git repo (preflight does this).
|
|
49
|
+
*/
|
|
50
|
+
targetRepoPath: string;
|
|
51
|
+
/**
|
|
52
|
+
* `process.cwd()` for the spawned autonomous child. Defaults to the
|
|
53
|
+
* first item's worktree path. The autonomous child opens additional
|
|
54
|
+
* worktrees as it works through items.
|
|
55
|
+
*/
|
|
56
|
+
initialWorkdir?: string;
|
|
57
|
+
/**
|
|
58
|
+
* Command + args to spawn. Default is the production invocation:
|
|
59
|
+
* `claude --skill autonomous` (resolved via PATH). Tests pass a
|
|
60
|
+
* harmless command (`bash -c "exit 0"`) so the test doesn't actually
|
|
61
|
+
* invoke claude.
|
|
62
|
+
*
|
|
63
|
+
* Stop condition + project/round identifiers are passed via env:
|
|
64
|
+
* INSTAR_PROJECT_ID, INSTAR_ROUND_INDEX, INSTAR_STOP_CONDITION
|
|
65
|
+
* INSTAR_ROUND_ITEM_IDS (JSON-encoded array)
|
|
66
|
+
*/
|
|
67
|
+
spawnCommand?: string;
|
|
68
|
+
spawnArgs?: string[];
|
|
69
|
+
/** Poll cadence; defaults to 60s. */
|
|
70
|
+
pollIntervalMs?: number;
|
|
71
|
+
/** SIGTERM→SIGKILL grace; defaults to 5s. */
|
|
72
|
+
sigtermGraceMs?: number;
|
|
73
|
+
/** Max resume attempts on transient failures; defaults to 3. */
|
|
74
|
+
maxResumeAttempts?: number;
|
|
75
|
+
/**
|
|
76
|
+
* Test hook: shell out to verify per-item artifacts. Defaults to a
|
|
77
|
+
* real `gh pr view` shell-out via SafeGitExecutor. Tests inject a
|
|
78
|
+
* stub. The function returns the set of itemIds whose artifact
|
|
79
|
+
* is verified merged-on-main-with-CI-green.
|
|
80
|
+
*/
|
|
81
|
+
verifyMergedItems?: (childIds: string[]) => Promise<Set<string>>;
|
|
82
|
+
}
|
|
83
|
+
export type RoundOutcome = 'complete' | 'partially-complete' | 'failed' | 'halted';
|
|
84
|
+
export interface RunRoundResult {
|
|
85
|
+
outcome: RoundOutcome;
|
|
86
|
+
/** Verified-merged itemIds. */
|
|
87
|
+
mergedItemIds: string[];
|
|
88
|
+
/** itemIds that did NOT verify (only populated for partially-complete / halted). */
|
|
89
|
+
unmergedItemIds: string[];
|
|
90
|
+
/** Number of times the runner relaunched the child due to dynamic-stop changes. */
|
|
91
|
+
relaunchCount: number;
|
|
92
|
+
/** Number of times the round resumed after a transient child failure. */
|
|
93
|
+
resumeAttempts: number;
|
|
94
|
+
/** Human-readable reason — useful for halted / failed outcomes. */
|
|
95
|
+
reason?: string;
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Lock that the runner needs but is too high-level to require here as
|
|
99
|
+
* a positional parameter. Wires through the standard
|
|
100
|
+
* `.instar/local/round-runner.lock` location.
|
|
101
|
+
*/
|
|
102
|
+
export interface RunRoundDeps {
|
|
103
|
+
stateDir: string;
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Run one round to completion. Caller is expected to preflight first.
|
|
107
|
+
*/
|
|
108
|
+
export declare function runRound(input: RunRoundInput, deps: RunRoundDeps): Promise<RunRoundResult>;
|
|
109
|
+
/** Helper exported so callers can wire a SafeGit-backed verifier. */
|
|
110
|
+
export declare function verifyMergedItemsViaGit(targetRepoPath: string, childIds: string[], tracker: InitiativeTracker): Promise<Set<string>>;
|
|
111
|
+
//# sourceMappingURL=ProjectRoundExecution.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ProjectRoundExecution.d.ts","sourceRoot":"","sources":["../../src/core/ProjectRoundExecution.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkCG;AAIH,OAAO,KAAK,EAAc,iBAAiB,EAAe,MAAM,wBAAwB,CAAC;AAKzF,sEAAsE;AACtE,eAAO,MAAM,wBAAwB,QAAS,CAAC;AAC/C,eAAO,MAAM,wBAAwB,OAAQ,CAAC;AAC9C,gDAAgD;AAChD,eAAO,MAAM,2BAA2B,IAAI,CAAC;AAE7C,MAAM,WAAW,aAAa;IAC5B,OAAO,EAAE,iBAAiB,CAAC;IAC3B,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB;;;OAGG;IACH,cAAc,EAAE,MAAM,CAAC;IAEvB;;;;OAIG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC;IAExB;;;;;;;;;OASG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,SAAS,CAAC,EAAE,MAAM,EAAE,CAAC;IAErB,qCAAqC;IACrC,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,6CAA6C;IAC7C,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,gEAAgE;IAChE,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAE3B;;;;;OAKG;IACH,iBAAiB,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,KAAK,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC;CAClE;AAED,MAAM,MAAM,YAAY,GAAG,UAAU,GAAG,oBAAoB,GAAG,QAAQ,GAAG,QAAQ,CAAC;AAEnF,MAAM,WAAW,cAAc;IAC7B,OAAO,EAAE,YAAY,CAAC;IACtB,+BAA+B;IAC/B,aAAa,EAAE,MAAM,EAAE,CAAC;IACxB,oFAAoF;IACpF,eAAe,EAAE,MAAM,EAAE,CAAC;IAC1B,mFAAmF;IACnF,aAAa,EAAE,MAAM,CAAC;IACtB,yEAAyE;IACzE,cAAc,EAAE,MAAM,CAAC;IACvB,mEAAmE;IACnE,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED;;;;GAIG;AACH,MAAM,WAAW,YAAY;IAC3B,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED;;GAEG;AACH,wBAAsB,QAAQ,CAAC,KAAK,EAAE,aAAa,EAAE,IAAI,EAAE,YAAY,GAAG,OAAO,CAAC,cAAc,CAAC,CA2JhG;AAqMD,qEAAqE;AACrE,wBAAsB,uBAAuB,CAC3C,cAAc,EAAE,MAAM,EACtB,QAAQ,EAAE,MAAM,EAAE,EAClB,OAAO,EAAE,iBAAiB,GACzB,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,CAgBtB"}
|