@sublang/playbook 17.3.0 → 17.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -2
- package/docs/configuration.md +45 -4
- package/docs/embedding.md +94 -0
- package/package.json +34 -5
- package/reference/sdlc/captain.md +6 -4
- package/reference/sdlc/captain.playbook/captain.fsm.d.ts +5 -0
- package/reference/sdlc/captain.playbook/captain.fsm.js +4 -3
- package/reference/sdlc/captain.playbook/captain.fsm.ts +9 -4
- package/reference/sdlc/captain.playbook/captain.gears.md +6 -3
- package/reference/sdlc/captain.playbook/captain.playbook.d.ts +2 -0
- package/reference/sdlc/captain.playbook/captain.playbook.js +7 -3
- package/reference/sdlc/captain.playbook/captain.playbook.ts +8 -4
- package/reference/sdlc/code.md +24 -4
- package/reference/sdlc/code.playbook/asset-types.d.ts +32 -0
- package/reference/sdlc/code.playbook/bin/launch-config.js +2 -0
- package/reference/sdlc/code.playbook/bin/portable-codec.js +12 -5
- package/reference/sdlc/code.playbook/bin/run.js +68 -4
- package/reference/sdlc/code.playbook/bin/session-host.js +16 -4
- package/reference/sdlc/code.playbook/bin/session-media.js +46 -0
- package/reference/sdlc/code.playbook/bin/session-store.js +145 -13
- package/reference/sdlc/code.playbook/code.fsm.d.ts +12 -0
- package/reference/sdlc/code.playbook/code.fsm.js +40 -6
- package/reference/sdlc/code.playbook/code.fsm.ts +53 -6
- package/reference/sdlc/code.playbook/code.gears.md +32 -4
- package/reference/sdlc/code.playbook/playbook-captain.d.ts +17 -1
- package/reference/sdlc/code.playbook/playbook-captain.js +201 -19
- package/reference/sdlc/code.playbook/playbook-captain.ts +224 -24
- package/reference/sdlc/code.playbook/playbook.config.template.yaml +16 -3
- package/reference/sdlc/code.playbook/session-assets.d.ts +53 -0
- package/reference/sdlc/code.playbook/session-assets.js +303 -0
- package/reference/sdlc/code.playbook/session-host.d.ts +9 -1
- package/reference/sdlc/code.playbook/session-store.d.ts +26 -2
- package/reference/sdlc/code.playbook/worker-evidence.d.ts +8 -0
- package/reference/sdlc/code.playbook/worker-evidence.js +51 -0
- package/reference/sdlc/inspect.md +40 -0
- package/reference/sdlc/inspect.playbook/inspect.fsm.d.ts +168 -0
- package/reference/sdlc/inspect.playbook/inspect.fsm.js +354 -0
- package/reference/sdlc/inspect.playbook/inspect.fsm.ts +455 -0
- package/reference/sdlc/inspect.playbook/inspect.gears.md +36 -0
- package/reference/sdlc/inspect.playbook/inspect.playbook.d.ts +17 -0
- package/reference/sdlc/inspect.playbook/inspect.playbook.js +142 -0
- package/reference/sdlc/inspect.playbook/inspect.playbook.ts +184 -0
- package/reference/sdlc/inspect.playbook/inspect.registry.d.ts +41 -0
- package/reference/sdlc/inspect.playbook/inspect.registry.js +51 -0
- package/reference/sdlc/inspect.playbook/inspect.registry.ts +110 -0
- package/slc/gears2fsm.md +17 -3
- package/slc/text2gears.md +7 -0
- package/slc/workflow-contracts.json +43 -1
- package/src/xstate-playbook-runtime.js +5 -0
- package/src/xstate-playbook-runtime.ts +7 -0
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
// complete retained-generation map under guarded source-first publication.
|
|
8
8
|
|
|
9
9
|
import { createHash, randomUUID } from 'node:crypto';
|
|
10
|
+
import { createAssetStore, parseAssetUri, validateAssetRef, validateAssetRefs } from '../session-assets.js';
|
|
10
11
|
import {
|
|
11
12
|
SESSION_MANIFEST_VERSION, EMPTY_REPLAY_SHA256, sha256,
|
|
12
13
|
validateSessionManifest, recoveryFromManifest, manifestFromRecovery,
|
|
@@ -637,6 +638,12 @@ export function createCaptainSessionStore(options = {}) {
|
|
|
637
638
|
const replayReadCursors = new Map();
|
|
638
639
|
const replayReadQueues = new Map();
|
|
639
640
|
const portableWriters = new Map();
|
|
641
|
+
const assetStores = new Map();
|
|
642
|
+
const assetStoreFor = (sessionId) => {
|
|
643
|
+
assertSessionId(sessionId);
|
|
644
|
+
if (!assetStores.has(sessionId)) assetStores.set(sessionId, createAssetStore({ directory: join(sessionsDir, `${sessionId}.assets`), ...(options.maxAssetBytes === undefined ? {} : { maxAssetBytes: options.maxAssetBytes }) }));
|
|
645
|
+
return assetStores.get(sessionId);
|
|
646
|
+
};
|
|
640
647
|
|
|
641
648
|
if (!isAbsolute(sessionsDir)) {
|
|
642
649
|
throw new Error('Captain session store path must be absolute');
|
|
@@ -758,7 +765,11 @@ export function createCaptainSessionStore(options = {}) {
|
|
|
758
765
|
if (value.sessionId !== sessionId) throw new Error('session manifest identity does not match its filename');
|
|
759
766
|
return value.schemaVersion === 7 ? validateSessionManifest(value) : value;
|
|
760
767
|
};
|
|
761
|
-
const prepare = () =>
|
|
768
|
+
const prepare = async () => {
|
|
769
|
+
await prepareSessionPermissions(sessionsDir, fs);
|
|
770
|
+
let names; try { names = await fs.readdir(sessionsDir); } catch (cause) { if (cause?.code === 'ENOENT') return; throw cause; }
|
|
771
|
+
for (const name of names) if (name.endsWith('.assets') && SESSION_ID_PATTERN.test(name.slice(0, -7))) await assetStoreFor(name.slice(0, -7)).prepare();
|
|
772
|
+
};
|
|
762
773
|
const readHistory = async (sessionId, options = {}) => {
|
|
763
774
|
assertSessionId(sessionId);
|
|
764
775
|
const history = await readSessionHistory({ sessionsDir, path: recordsPathFor(sessionId), fs, afterSeq: options.afterSeq ?? 0 });
|
|
@@ -771,6 +782,35 @@ export function createCaptainSessionStore(options = {}) {
|
|
|
771
782
|
} catch { return history; }
|
|
772
783
|
return legacyJournalHistory(legacy, options.afterSeq ?? 0, history);
|
|
773
784
|
};
|
|
785
|
+
// Only our typed reference locations participate; opaque tool/text data is history.
|
|
786
|
+
const assertAssetInventory = (manifest, entries) => {
|
|
787
|
+
const declared = new Map((manifest.assets?.entries ?? []).map((reference) => [reference.assetId, reference]));
|
|
788
|
+
const check = (reference) => {
|
|
789
|
+
const valid = validateAssetRef(reference);
|
|
790
|
+
if (declared.get(valid.assetId)?.byteLength !== valid.byteLength) throw new Error(`session asset reference is absent from its checkpoint inventory: ${valid.assetId}`);
|
|
791
|
+
};
|
|
792
|
+
const checkRefs = (references) => { for (const reference of references ?? []) check(reference); };
|
|
793
|
+
const snapshot = (value) => {
|
|
794
|
+
if (!value) return;
|
|
795
|
+
checkRefs(value.pendingAttachments);
|
|
796
|
+
for (const frame of value.frames ?? []) checkRefs(frame.attachments);
|
|
797
|
+
for (const item of value.journal ?? []) if (item.kind === 'boss' && typeof item.payload === 'object' && item.payload !== null) checkRefs(item.payload.attachments);
|
|
798
|
+
};
|
|
799
|
+
snapshot(manifest.snapshot); snapshot(manifest.uncertain?.progress?.snapshot);
|
|
800
|
+
checkRefs(manifest.uncertain?.attachments);
|
|
801
|
+
for (const generation of Object.values(manifest.retainedGenerations ?? {})) for (const frame of generation.frames ?? []) checkRefs(frame.attachments);
|
|
802
|
+
for (const { record } of entries) {
|
|
803
|
+
if (record.type === 'turn_started') checkRefs(record.turn?.attachments);
|
|
804
|
+
else if (record.type === 'playbook_evidence') check(record.asset);
|
|
805
|
+
else if (record.type === 'player_event' || record.type === 'captain_event') {
|
|
806
|
+
const event = record.event;
|
|
807
|
+
if (event?.type === 'media' && event.payload?.source?.type === 'uri') {
|
|
808
|
+
const id = parseAssetUri(event.payload.source.uri);
|
|
809
|
+
if (id && !declared.has(id)) throw new Error(`session media asset is absent from its checkpoint inventory: ${id}`);
|
|
810
|
+
} else if (event?.type === 'tool_result' && event.payload?.output?.type === 'asset_reference') check(event.payload.output.asset);
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
};
|
|
774
814
|
const validate = async (sessionId, context = {}) => {
|
|
775
815
|
const manifest = await readManifest(sessionId);
|
|
776
816
|
const history = await readHistory(sessionId);
|
|
@@ -790,6 +830,12 @@ export function createCaptainSessionStore(options = {}) {
|
|
|
790
830
|
integrityValid = false;
|
|
791
831
|
reasons.push('session replay contains provider continuation fields');
|
|
792
832
|
}
|
|
833
|
+
try { assertAssetInventory(manifest, history.entries.filter(({ seq }) => seq <= manifest.replay.seq)); }
|
|
834
|
+
catch (cause) { integrityValid = false; reasons.push(errorMessage(cause)); }
|
|
835
|
+
for (const reference of manifest.assets?.entries ?? []) {
|
|
836
|
+
try { const reader = await assetStoreFor(sessionId).openAsset(reference); await reader.close(); }
|
|
837
|
+
catch (cause) { integrityValid = false; reasons.push(`session asset is unavailable: ${errorMessage(cause)}`); }
|
|
838
|
+
}
|
|
793
839
|
if (manifest.state === 'history-only') reasons.push(manifest.reason);
|
|
794
840
|
if (manifest.replay.incomplete || history.incomplete) reasons.push('session replay is incomplete');
|
|
795
841
|
if (history.digests[manifest.replay.seq] !== manifest.replay.sha256) { integrityValid = false; reasons.push('session replay checkpoint digest does not match'); }
|
|
@@ -1111,6 +1157,24 @@ export function createCaptainSessionStore(options = {}) {
|
|
|
1111
1157
|
};
|
|
1112
1158
|
|
|
1113
1159
|
const deleteRecord = async (sessionId) => {
|
|
1160
|
+
const assetsDirectory = join(sessionsDir, `${sessionId}.assets`);
|
|
1161
|
+
try {
|
|
1162
|
+
await assertPrivateDirectory(assetsDirectory, fs);
|
|
1163
|
+
const names = await fs.readdir(assetsDirectory);
|
|
1164
|
+
const owned = names.filter((name) => /^(?:[0-9a-f]{64}(?:\.json)?|\.import-[0-9a-f-]+\.tmp)$/.test(name));
|
|
1165
|
+
for (const name of owned) await assertPrivateRegularPath(join(assetsDirectory, name), 0o600, fs, 'session asset');
|
|
1166
|
+
for (const name of owned) await fs.unlink(join(assetsDirectory, name));
|
|
1167
|
+
// Foreign regular files (Finder or editor files) go with the session; a
|
|
1168
|
+
// remaining subdirectory or link keeps the directory, not the session.
|
|
1169
|
+
for (const name of names.filter((entry) => !owned.includes(entry))) {
|
|
1170
|
+
const path = join(assetsDirectory, name);
|
|
1171
|
+
try { const stat = await fs.lstat(path); if (stat.isFile() && !stat.isSymbolicLink()) await fs.unlink(path); }
|
|
1172
|
+
catch (cause) { if (cause?.code !== 'ENOENT') throw cause; }
|
|
1173
|
+
}
|
|
1174
|
+
try { await fs.rmdir(assetsDirectory); }
|
|
1175
|
+
catch (cause) { if (cause?.code !== 'ENOTEMPTY' && cause?.code !== 'EEXIST') throw cause; }
|
|
1176
|
+
await syncDirectory(sessionsDir, fs);
|
|
1177
|
+
} catch (cause) { if (cause?.code !== 'ENOENT') throw cause; }
|
|
1114
1178
|
for (const suffix of ['.records.jsonl', '.hints.json', '.spex.json', '.json']) {
|
|
1115
1179
|
const path = join(sessionsDir, `${sessionId}${suffix}`);
|
|
1116
1180
|
try { await assertPrivateRegularPath(path, 0o600, fs, 'session file'); await fs.unlink(path); await syncDirectory(sessionsDir, fs); }
|
|
@@ -1396,6 +1460,7 @@ export function createCaptainSessionStore(options = {}) {
|
|
|
1396
1460
|
const acquire = async (sessionId, management = false) => {
|
|
1397
1461
|
assertSessionId(sessionId);
|
|
1398
1462
|
await prepareSessionPermissions(sessionsDir, fs, sessionId);
|
|
1463
|
+
await assetStoreFor(sessionId).prepare();
|
|
1399
1464
|
let stage;
|
|
1400
1465
|
let stagePublished = false;
|
|
1401
1466
|
try {
|
|
@@ -1451,6 +1516,7 @@ export function createCaptainSessionStore(options = {}) {
|
|
|
1451
1516
|
readHistory,
|
|
1452
1517
|
validateSession: validate,
|
|
1453
1518
|
portableWriters,
|
|
1519
|
+
assetStore: assetStoreFor(sessionId),
|
|
1454
1520
|
writeRecord,
|
|
1455
1521
|
syncRecordDirectory: () => syncDirectory(sessionsDir, fs),
|
|
1456
1522
|
deleteRecord,
|
|
@@ -1684,8 +1750,39 @@ export function createCaptainSessionStore(options = {}) {
|
|
|
1684
1750
|
finally { await lease.release(); }
|
|
1685
1751
|
};
|
|
1686
1752
|
|
|
1753
|
+
const exportBundle = async (sessionId) => {
|
|
1754
|
+
assertSessionId(sessionId);
|
|
1755
|
+
for (let attempt = 0; attempt < 3; attempt++) {
|
|
1756
|
+
const manifestText = await readPrivateRegularFile(recordPathFor(sessionId), 0o600, fs, 'record');
|
|
1757
|
+
const manifest = validateSessionManifest(JSON.parse(manifestText));
|
|
1758
|
+
const replayBytes = Buffer.from(await readPrivateRegularFile(recordsPathFor(sessionId), 0o600, fs, 'replay stream'), 'utf8');
|
|
1759
|
+
let end = 0;
|
|
1760
|
+
for (let seq = 0; seq < manifest.replay.seq; seq++) {
|
|
1761
|
+
const next = replayBytes.indexOf(10, end);
|
|
1762
|
+
if (next === -1) throw new Error('session bundle is missing its checkpoint replay prefix');
|
|
1763
|
+
end = next + 1;
|
|
1764
|
+
}
|
|
1765
|
+
const replay = replayBytes.subarray(0, end);
|
|
1766
|
+
if (sha256(replay) !== manifest.replay.sha256 || manifest.replay.incomplete) throw new Error('session bundle replay checkpoint is incomplete or corrupt');
|
|
1767
|
+
assertAssetInventory(manifest, parseSessionHistory(replay).entries);
|
|
1768
|
+
const assets = [];
|
|
1769
|
+
for (const reference of manifest.assets?.entries ?? []) {
|
|
1770
|
+
const reader = await assetStoreFor(sessionId).openAsset(reference); await reader.close();
|
|
1771
|
+
const hash = reference.assetId.slice(7);
|
|
1772
|
+
assets.push(Object.freeze({ assetId: reference.assetId, path: hash, metadataPath: `${hash}.json`, byteLength: reference.byteLength }));
|
|
1773
|
+
}
|
|
1774
|
+
if (manifestText !== await readPrivateRegularFile(recordPathFor(sessionId), 0o600, fs, 'record')) continue;
|
|
1775
|
+
return Object.freeze({ sessionId, manifest: Buffer.from(manifestText, 'utf8'), replay, assets: Object.freeze(assets) });
|
|
1776
|
+
}
|
|
1777
|
+
throw new Error('session changed while exporting its bundle; retry');
|
|
1778
|
+
};
|
|
1779
|
+
|
|
1687
1780
|
return Object.freeze({
|
|
1688
1781
|
sessionsDir,
|
|
1782
|
+
readAsset: (sessionId, reference, options) => assetStoreFor(sessionId).readAsset(reference, options),
|
|
1783
|
+
openAsset: (sessionId, reference, options) => assetStoreFor(sessionId).openAsset(reference, options),
|
|
1784
|
+
describeAsset: (sessionId, assetId) => assetStoreFor(sessionId).describeAsset(assetId),
|
|
1785
|
+
exportBundle,
|
|
1689
1786
|
prepare,
|
|
1690
1787
|
migrate,
|
|
1691
1788
|
migrateLegacyDefault,
|
|
@@ -2540,6 +2637,7 @@ async function createLease({
|
|
|
2540
2637
|
readHistory,
|
|
2541
2638
|
validateSession,
|
|
2542
2639
|
portableWriters,
|
|
2640
|
+
assetStore,
|
|
2543
2641
|
writeRecord,
|
|
2544
2642
|
syncRecordDirectory,
|
|
2545
2643
|
deleteRecord,
|
|
@@ -2636,7 +2734,9 @@ async function createLease({
|
|
|
2636
2734
|
const seq = history.digests[durableSeq] === undefined ? previousManifest?.replay?.seq ?? 0 : durableSeq;
|
|
2637
2735
|
const digest = history.digests[seq] ?? previousManifest?.replay?.sha256 ?? EMPTY_REPLAY_SHA256;
|
|
2638
2736
|
const replay = { seq, sha256: digest, incomplete: previousManifest?.replay?.incomplete === true || status.incomplete || history.incomplete || history.digests[seq] === undefined };
|
|
2639
|
-
const
|
|
2737
|
+
const entries = await assetStore.listAssets();
|
|
2738
|
+
const assets = entries.length === 0 ? undefined : { version: 1, entries: await Promise.all(entries.map(({ assetId }) => assetStore.describeAsset(assetId))) };
|
|
2739
|
+
const manifest = manifestFromRecovery(recovery, replay, contextSeq, assets);
|
|
2640
2740
|
return manifest;
|
|
2641
2741
|
};
|
|
2642
2742
|
portableWriters.set(sessionId, { checkpoint, sync: replayWriter.checkpoint, published: (manifest) => { previousManifest = manifest; } });
|
|
@@ -2928,6 +3028,8 @@ async function createLease({
|
|
|
2928
3028
|
source.effectLedger,
|
|
2929
3029
|
);
|
|
2930
3030
|
|
|
3031
|
+
const inheritedAssets = new Map(Object.values(retainedGenerations).flatMap((generation) => generation.frames.flatMap((frame) => frame.attachments ?? [])).map((reference) => [reference.assetId, reference]));
|
|
3032
|
+
for (const reference of inheritedAssets.values()) await assetStore.copyAsset(sourceLease, reference);
|
|
2931
3033
|
await sourceLease.assertOwner();
|
|
2932
3034
|
await assertOwnerUnchecked();
|
|
2933
3035
|
let sourcePublished = false;
|
|
@@ -3026,11 +3128,12 @@ async function createLease({
|
|
|
3026
3128
|
|
|
3027
3129
|
const beginTurn = ({
|
|
3028
3130
|
input,
|
|
3131
|
+
attachments,
|
|
3029
3132
|
attemptId,
|
|
3030
3133
|
attemptedExecutionProjection,
|
|
3031
3134
|
} = {}) =>
|
|
3032
3135
|
runExclusive(async () => {
|
|
3033
|
-
assertAcceptedInput(input);
|
|
3136
|
+
assertAcceptedInput(input, attachments);
|
|
3034
3137
|
assertUuid(attemptId, 'Captain session attempt id');
|
|
3035
3138
|
const attempted = validateCaptainSessionExecutionProjection(
|
|
3036
3139
|
attemptedExecutionProjection,
|
|
@@ -3065,6 +3168,7 @@ async function createLease({
|
|
|
3065
3168
|
uncertain: {
|
|
3066
3169
|
baseUpdatedAt: prior.updatedAt,
|
|
3067
3170
|
input,
|
|
3171
|
+
...(attachments?.length ? { attachments: validateAssetRefs(attachments) } : {}),
|
|
3068
3172
|
attemptId,
|
|
3069
3173
|
attemptNumber: 1,
|
|
3070
3174
|
markedAt: timestamp,
|
|
@@ -3119,6 +3223,7 @@ async function createLease({
|
|
|
3119
3223
|
baseUpdatedAt: prior.uncertain.baseUpdatedAt,
|
|
3120
3224
|
...(prior.uncertain.progress === undefined ? {} : { progress: prior.uncertain.progress }),
|
|
3121
3225
|
input: prior.uncertain.input,
|
|
3226
|
+
...(prior.uncertain.attachments === undefined ? {} : { attachments: prior.uncertain.attachments }),
|
|
3122
3227
|
attemptId: nextAttemptId,
|
|
3123
3228
|
attemptNumber: prior.uncertain.attemptNumber + 1,
|
|
3124
3229
|
markedAt: timestamp,
|
|
@@ -3637,11 +3742,11 @@ async function createLease({
|
|
|
3637
3742
|
if (Object.hasOwn(step, 'result') && !['completion', 'answer'].includes(step.kind)) throw new Error('A step result requires its saved start');
|
|
3638
3743
|
progress.steps.push(step);
|
|
3639
3744
|
} else {
|
|
3640
|
-
const { result, ...start } = step;
|
|
3745
|
+
const { result, workerEvidence, ...start } = step;
|
|
3641
3746
|
const saved = progress.steps[index];
|
|
3642
|
-
const { result: priorResult, ...priorStart } = saved;
|
|
3747
|
+
const { result: priorResult, workerEvidence: priorEvidence, ...priorStart } = saved;
|
|
3643
3748
|
if (!isDeepStrictEqual(start, priorStart) ||
|
|
3644
|
-
(priorResult !== undefined && !isDeepStrictEqual(priorResult, result))) {
|
|
3749
|
+
(priorResult !== undefined && (!isDeepStrictEqual(priorResult, result) || !isDeepStrictEqual(priorEvidence, workerEvidence)))) {
|
|
3645
3750
|
throw new Error('A saved step cannot be replaced');
|
|
3646
3751
|
}
|
|
3647
3752
|
progress.steps[index] = step;
|
|
@@ -3728,6 +3833,11 @@ async function createLease({
|
|
|
3728
3833
|
sessionId,
|
|
3729
3834
|
ownerToken: owner.ownerToken,
|
|
3730
3835
|
append,
|
|
3836
|
+
importAsset: (input) => runExclusive(async () => { await assertOwnerUnchecked(); return assetStore.importAsset(input); }),
|
|
3837
|
+
resolveAttachments: (references, options) => runExclusive(async () => { await assertOwnerUnchecked(); if (!Array.isArray(references)) throw new TypeError('attachments must be an array'); return Promise.all(references.map((reference) => assetStore.resolveAttachment(reference, options))); }),
|
|
3838
|
+
readAsset: (reference, options) => assetStore.readAsset(reference, options),
|
|
3839
|
+
openAsset: (reference, options) => assetStore.openAsset(reference, options),
|
|
3840
|
+
describeAsset: (assetId) => assetStore.describeAsset(assetId),
|
|
3731
3841
|
recordContext,
|
|
3732
3842
|
consumeHints,
|
|
3733
3843
|
acknowledgeHint,
|
|
@@ -4089,7 +4199,7 @@ function validateCanonicalCaptainSessionRecord(
|
|
|
4089
4199
|
exactOptionalKeys(
|
|
4090
4200
|
uncertain,
|
|
4091
4201
|
UNCERTAIN_KEYS,
|
|
4092
|
-
['abandonment', 'progress'],
|
|
4202
|
+
['abandonment', 'progress', 'attachments'],
|
|
4093
4203
|
'Captain session record uncertain',
|
|
4094
4204
|
);
|
|
4095
4205
|
if (uncertain.baseUpdatedAt !== null) {
|
|
@@ -4110,7 +4220,7 @@ function validateCanonicalCaptainSessionRecord(
|
|
|
4110
4220
|
);
|
|
4111
4221
|
}
|
|
4112
4222
|
}
|
|
4113
|
-
assertAcceptedInput(uncertain.input);
|
|
4223
|
+
assertAcceptedInput(uncertain.input, uncertain.attachments);
|
|
4114
4224
|
assertUuid(uncertain.attemptId, 'Captain session attempt id');
|
|
4115
4225
|
if (
|
|
4116
4226
|
!Number.isSafeInteger(uncertain.attemptNumber) ||
|
|
@@ -4172,8 +4282,9 @@ function validateCanonicalCaptainSessionRecord(
|
|
|
4172
4282
|
exactOptionalKeys(progress, ['snapshot', 'steps'], ['positionStepId'], 'Captain progress');
|
|
4173
4283
|
if (!Array.isArray(progress.steps)) throw new Error('Captain steps must be an array');
|
|
4174
4284
|
const ids = new Set();
|
|
4285
|
+
const workerBoundaryIds = new Set();
|
|
4175
4286
|
for (const step of progress.steps) {
|
|
4176
|
-
exactOptionalKeys(requireRecord(step, 'Captain step'), ['id', 'kind', 'stateId', 'runtimeSessionId', 'playbookId'], ['result'], 'Captain step');
|
|
4287
|
+
exactOptionalKeys(requireRecord(step, 'Captain step'), ['id', 'kind', 'stateId', 'runtimeSessionId', 'playbookId'], ['result', 'workerEvidence'], 'Captain step');
|
|
4177
4288
|
assertUuid(step.id, 'Captain step id');
|
|
4178
4289
|
assertUuid(step.runtimeSessionId, 'Captain step runtime');
|
|
4179
4290
|
if (ids.has(step.id)) throw new Error('Captain step ids must be unique');
|
|
@@ -4209,6 +4320,22 @@ function validateCanonicalCaptainSessionRecord(
|
|
|
4209
4320
|
}
|
|
4210
4321
|
requireCanonicalNonblank(step.stateId, 'Captain step state');
|
|
4211
4322
|
if (!Object.hasOwn(structural.catalog, step.playbookId)) throw new Error('Captain step names an unknown playbook');
|
|
4323
|
+
if (step.workerEvidence !== undefined) {
|
|
4324
|
+
const evidence = requireRecord(step.workerEvidence, 'Worker evidence');
|
|
4325
|
+
exactOptionalKeys(evidence, ['boundaryId', 'playerId'], [], 'Worker evidence');
|
|
4326
|
+
assertUuid(evidence.boundaryId, 'Worker evidence boundary');
|
|
4327
|
+
requireCanonicalNonblank(evidence.playerId, 'Worker evidence player');
|
|
4328
|
+
const boundary = record.effectLedger.boundaries.find((entry) => entry.boundaryId === evidence.boundaryId);
|
|
4329
|
+
if (step.kind !== 'player' || !step.result || typeof step.result !== 'object' || Array.isArray(step.result) ||
|
|
4330
|
+
!boundary || boundary.playbookId !== step.playbookId || boundary.sourceStateId !== step.stateId ||
|
|
4331
|
+
workerBoundaryIds.has(evidence.boundaryId) || isDeepStrictEqual(boundary, record.snapshot.effectLedger.boundaries[boundary.sequence - 1]) ||
|
|
4332
|
+
!boundary.physicalReceipt || !boundary.finalText?.trim() ||
|
|
4333
|
+
boundary.semanticCandidate?.guard !== step.result.guard || typeof step.result.guard !== 'string' || step.result.guard === 'needsBossReply' ||
|
|
4334
|
+
structural.catalog[step.playbookId]?.roles?.[boundary.roleId]?.playerId !== evidence.playerId) {
|
|
4335
|
+
throw new Error('Worker evidence must name its accepted player boundary');
|
|
4336
|
+
}
|
|
4337
|
+
workerBoundaryIds.add(evidence.boundaryId);
|
|
4338
|
+
}
|
|
4212
4339
|
}
|
|
4213
4340
|
if (progress.positionStepId !== undefined && progress.positionStepId !== null && !ids.has(progress.positionStepId)) throw new Error('Captain progress positionStepId must name a recorded step');
|
|
4214
4341
|
if (progress.snapshot === null && progress.positionStepId != null) throw new Error('Captain progress positionStepId requires a saved position');
|
|
@@ -5312,7 +5439,7 @@ function assertReleasedSchema2CaptainSessionRecord(record) {
|
|
|
5312
5439
|
);
|
|
5313
5440
|
}
|
|
5314
5441
|
}
|
|
5315
|
-
assertAcceptedInput(uncertain.input);
|
|
5442
|
+
assertAcceptedInput(uncertain.input, uncertain.attachments);
|
|
5316
5443
|
assertUuid(uncertain.attemptId, 'Captain session attempt id');
|
|
5317
5444
|
if (
|
|
5318
5445
|
!Number.isSafeInteger(uncertain.attemptNumber) ||
|
|
@@ -5408,6 +5535,7 @@ function validateCaptainSessionProjection(
|
|
|
5408
5535
|
'fastMode',
|
|
5409
5536
|
'subagentModel',
|
|
5410
5537
|
'subagentEffort',
|
|
5538
|
+
'browser',
|
|
5411
5539
|
],
|
|
5412
5540
|
playerPath,
|
|
5413
5541
|
);
|
|
@@ -5578,6 +5706,7 @@ function validateProjectedAgent(value, path, { structural, hasId = false }) {
|
|
|
5578
5706
|
const agent = requireRecord(value, path);
|
|
5579
5707
|
if (!structural) {
|
|
5580
5708
|
validateProjectedFastMode(agent, `${path}.fastMode`);
|
|
5709
|
+
if (agent.browser !== undefined && typeof agent.browser !== 'boolean') throw new Error(`${path}.browser must be a boolean`);
|
|
5581
5710
|
validateProjectedSubagentTuning(agent, path);
|
|
5582
5711
|
}
|
|
5583
5712
|
if (!hasId) {
|
|
@@ -5592,6 +5721,7 @@ function validateProjectedAgent(value, path, { structural, hasId = false }) {
|
|
|
5592
5721
|
'fastMode',
|
|
5593
5722
|
'subagentModel',
|
|
5594
5723
|
'subagentEffort',
|
|
5724
|
+
'browser',
|
|
5595
5725
|
],
|
|
5596
5726
|
path,
|
|
5597
5727
|
);
|
|
@@ -6144,9 +6274,10 @@ function validateRetainedGenerationFrame(
|
|
|
6144
6274
|
'roleBindings',
|
|
6145
6275
|
'runtime',
|
|
6146
6276
|
],
|
|
6147
|
-
['parentSessionId', 'parentCallId', 'request', 'inputs'],
|
|
6277
|
+
['parentSessionId', 'parentCallId', 'request', 'inputs', 'attachments'],
|
|
6148
6278
|
path,
|
|
6149
6279
|
);
|
|
6280
|
+
if (frame.attachments !== undefined) validateAssetRefs(frame.attachments);
|
|
6150
6281
|
if (frame.request !== undefined) {
|
|
6151
6282
|
if (index !== 0) throw new Error(`${path}.request belongs only to the root`);
|
|
6152
6283
|
requireNonblank(frame.request, `${path}.request`);
|
|
@@ -6369,8 +6500,9 @@ async function requireUncertainRecord(record, attemptId) {
|
|
|
6369
6500
|
return record;
|
|
6370
6501
|
}
|
|
6371
6502
|
|
|
6372
|
-
function assertAcceptedInput(value) {
|
|
6373
|
-
if (
|
|
6503
|
+
function assertAcceptedInput(value, attachments) {
|
|
6504
|
+
if (attachments !== undefined) validateAssetRefs(attachments);
|
|
6505
|
+
if (typeof value !== 'string' || (value.trim().length === 0 && !attachments?.length)) {
|
|
6374
6506
|
throw new Error('Captain session input must be a non-empty string');
|
|
6375
6507
|
}
|
|
6376
6508
|
}
|
|
@@ -51,6 +51,8 @@ export interface IrTaskPhasePlayerInput extends PlayerInputBase {
|
|
|
51
51
|
readonly sourceItem: 'CODE-3';
|
|
52
52
|
/** `<ir-number>`: the IR whose next unfinished task this phase implements. */
|
|
53
53
|
readonly irNumber?: string;
|
|
54
|
+
/** Source-owned relay; historical canonical evidence, not present approval. */
|
|
55
|
+
readonly previousPhaseReview: string;
|
|
54
56
|
}
|
|
55
57
|
export type PlayerInput = FirstPhasePlayerInput | IrTaskPhasePlayerInput;
|
|
56
58
|
export type PlayerOutput = {
|
|
@@ -125,6 +127,14 @@ export interface CodeInput {
|
|
|
125
127
|
/** Optional seed for `<run-results>`; `START_CODE` may supply its own. */
|
|
126
128
|
readonly runResults?: string;
|
|
127
129
|
}
|
|
130
|
+
export interface PreviousPhaseReview {
|
|
131
|
+
readonly phaseKind: 'direct' | 'new-intent' | 'ir-task';
|
|
132
|
+
readonly phaseOutcome: PhaseOutcome;
|
|
133
|
+
readonly scopeCommit: string;
|
|
134
|
+
readonly evaluatedRevision: string;
|
|
135
|
+
readonly irNumber?: string;
|
|
136
|
+
readonly irTask?: string;
|
|
137
|
+
}
|
|
128
138
|
export interface CodeContext {
|
|
129
139
|
/** `<run-results>`: relevant run results relayed to every phase. */
|
|
130
140
|
readonly runResults: string;
|
|
@@ -142,6 +152,8 @@ export interface CodeContext {
|
|
|
142
152
|
readonly phaseOutcome?: PhaseOutcome;
|
|
143
153
|
/** Revision evaluated by the latest passing review. */
|
|
144
154
|
readonly evaluatedRevision?: string;
|
|
155
|
+
/** Frozen only by an accepted canonical clean child REVIEW. */
|
|
156
|
+
readonly previousPhaseReview?: PreviousPhaseReview;
|
|
145
157
|
/** Compact failure CODE reports when review did not pass a phase. */
|
|
146
158
|
readonly reviewError?: CompactError;
|
|
147
159
|
/** Sanitized evidence of the review result that ended CODE. */
|
|
@@ -25,13 +25,19 @@ const FIRST_PHASE_PROMPT = [
|
|
|
25
25
|
'For an existing IR, read the identified IR and implement exactly its next unfinished task, including corresponding tests or specs if any.',
|
|
26
26
|
'Do not implement a later task in this phase.',
|
|
27
27
|
"Mark the IR's progress and deliverables when relevant.",
|
|
28
|
+
"After implementing an IR task, identify it and state whether any IR task still has unfinished Coder work; if so identify the next unfinished task, otherwise state this was the final IR task's Coder work.",
|
|
29
|
+
"Report pending independent REVIEW, owner acceptance and later delivery/workshop steps separately; they imply an unfinished IR task only when the IR assigns that work to a remaining task.",
|
|
30
|
+
"Report only what the IR and completed work establish, in ordinary prose without a required marker.",
|
|
28
31
|
'If the IR will be finished after this phase, double-check that all acceptance criteria are met.',
|
|
29
32
|
'',
|
|
30
33
|
'Consult @specs/map.md for relevant context and @specs/meta.md for spec requirements, if needed.',
|
|
31
34
|
'',
|
|
32
35
|
'Keep to the original intent and follow what it asks.',
|
|
33
36
|
'Do not re-run tests or builds whose inputs have not changed since any previous reported run.',
|
|
34
|
-
"
|
|
37
|
+
"If a required Boss answer prevents completion of the current phase or IR task, ask that question and return without committing.",
|
|
38
|
+
"Preserve permitted uncommitted work for the deferred continuation; do not create a partial or blocker-only commit to meet the one-commit requirement.",
|
|
39
|
+
"After the answer allows completion, make exactly one phase-owned commit before REVIEW.",
|
|
40
|
+
"When the phase is complete, make its minimal changes and then one new commit, following @specs/packages/git.md; never amend an existing commit.",
|
|
35
41
|
'Make the commit message explain concisely what changed and why, including relevant verification.',
|
|
36
42
|
'Identify every new commit you make.',
|
|
37
43
|
'Credit every AI that contributed to this commit: Coder <coder-llm>.',
|
|
@@ -43,11 +49,20 @@ const IR_TASK_PHASE_PROMPT = [
|
|
|
43
49
|
'Read the identified IR and implement exactly its next unfinished task, including corresponding tests or specs if any.',
|
|
44
50
|
'Do not implement a later task in this phase.',
|
|
45
51
|
"Mark the IR's progress and deliverables when relevant.",
|
|
52
|
+
"After implementing an IR task, identify it and state whether any IR task still has unfinished Coder work; if so identify the next unfinished task, otherwise state this was the final IR task's Coder work.",
|
|
53
|
+
"Report pending independent REVIEW, owner acceptance and later delivery/workshop steps separately; they imply an unfinished IR task only when the IR assigns that work to a remaining task.",
|
|
54
|
+
"Report only what the IR and completed work establish, in ordinary prose without a required marker.",
|
|
46
55
|
'If the IR will be finished after this phase, double-check that all acceptance criteria are met.',
|
|
56
|
+
'Use the quoted previous-phase review only for its exact recorded scope; committed pending-review text may predate that canonical result.',
|
|
57
|
+
'Before treating it as current review evidence, verify clean current HEAD equals its evaluatedRevision; a mismatch is not approval.',
|
|
58
|
+
'It does not replace independent review of this task or any new owner or release decision.',
|
|
47
59
|
'',
|
|
48
60
|
'Keep to the original intent and follow what it asks.',
|
|
49
61
|
'Do not re-run tests or builds whose inputs have not changed since any previous reported run.',
|
|
50
|
-
"
|
|
62
|
+
"If a required Boss answer prevents completion of the current phase or IR task, ask that question and return without committing.",
|
|
63
|
+
"Preserve permitted uncommitted work for the deferred continuation; do not create a partial or blocker-only commit to meet the one-commit requirement.",
|
|
64
|
+
"After the answer allows completion, make exactly one phase-owned commit before REVIEW.",
|
|
65
|
+
"When the phase is complete, make its minimal changes and then one new commit, following @specs/packages/git.md; never amend an existing commit.",
|
|
51
66
|
'Make the commit message explain concisely what changed and why, including relevant verification.',
|
|
52
67
|
'Identify every new commit you make.',
|
|
53
68
|
'Credit every AI that contributed to this commit: Coder <coder-llm>.',
|
|
@@ -55,6 +70,7 @@ const IR_TASK_PHASE_PROMPT = [
|
|
|
55
70
|
'> Original request: <caller-input>',
|
|
56
71
|
'> IR number: <ir-number>',
|
|
57
72
|
'> Run results: <run-results>',
|
|
73
|
+
'> Previous phase review: <previous-phase-review>',
|
|
58
74
|
].join('\n');
|
|
59
75
|
const REVIEW_NEW_INTENT_PHASE_TEMPLATE = [
|
|
60
76
|
'> Original intent: <caller-input>',
|
|
@@ -68,7 +84,7 @@ const REVIEW_IR_TASK_PHASE_TEMPLATE = [
|
|
|
68
84
|
'',
|
|
69
85
|
'> Current IR task: <ir-task>',
|
|
70
86
|
].join('\n');
|
|
71
|
-
const NEEDS_BOSS_REPLY_DESCRIPTION = "
|
|
87
|
+
const NEEDS_BOSS_REPLY_DESCRIPTION = "At the end of this call, Coder has an unanswered clarifying question for Boss whose answer is required to complete the current coding phase. Earlier interim questions resolved or superseded by later affirmative current-phase completion, and prerequisites only for later review, acceptance or release stages, do not establish this outcome. Output shall include `question: <verbatim question text from the acting agent's prose>`.";
|
|
72
88
|
const FIRST_PHASE_RESULT = {
|
|
73
89
|
directCommit: "Coder completed the new coding intent as one direct implementation phase and made its one new commit; Coder's result affirmatively supports this outcome, and no fixed presentation format of the reply is required. Output shall include `coderOutput: <verbatim final text>` and `latestCommit: <commit identity>`.",
|
|
74
90
|
irCommit: "Coder decomposed the new coding intent into a new IR, implemented no IR task, and made its one new commit; Coder's result affirmatively supports this outcome, and no fixed presentation format of the reply is required. Output shall include `coderOutput: <verbatim final text>`, `latestCommit: <commit identity>`, and `irNumber` identifying the created IR.",
|
|
@@ -467,6 +483,7 @@ export const codeMachine = setup({
|
|
|
467
483
|
irTask: undefined,
|
|
468
484
|
phaseOutcome: undefined,
|
|
469
485
|
evaluatedRevision: undefined,
|
|
486
|
+
previousPhaseReview: undefined,
|
|
470
487
|
reviewError: undefined,
|
|
471
488
|
reviewEvidence: undefined,
|
|
472
489
|
lastError: undefined,
|
|
@@ -477,6 +494,7 @@ export const codeMachine = setup({
|
|
|
477
494
|
resetForInterrupt: assign({
|
|
478
495
|
phaseOutcome: undefined,
|
|
479
496
|
evaluatedRevision: undefined,
|
|
497
|
+
previousPhaseReview: undefined,
|
|
480
498
|
reviewError: undefined,
|
|
481
499
|
reviewEvidence: undefined,
|
|
482
500
|
lastError: undefined,
|
|
@@ -516,11 +534,24 @@ export const codeMachine = setup({
|
|
|
516
534
|
pendingBossQuestion: undefined,
|
|
517
535
|
bossReply: undefined,
|
|
518
536
|
}),
|
|
519
|
-
rememberReviewPass: assign(({ event }) => {
|
|
537
|
+
rememberReviewPass: assign(({ context, event }) => {
|
|
520
538
|
const pass = reviewPassOf(event);
|
|
521
|
-
return pass === undefined
|
|
539
|
+
return pass === undefined || context.codeCommit === undefined || context.phaseOutcome === undefined
|
|
522
540
|
? {}
|
|
523
|
-
: {
|
|
541
|
+
: {
|
|
542
|
+
evaluatedRevision: pass.evaluatedRevision,
|
|
543
|
+
previousPhaseReview: {
|
|
544
|
+
phaseKind: context.phaseOutcome === 'directCommit'
|
|
545
|
+
? 'direct' : context.phaseOutcome === 'irCommit'
|
|
546
|
+
? 'new-intent' : 'ir-task',
|
|
547
|
+
phaseOutcome: context.phaseOutcome,
|
|
548
|
+
scopeCommit: context.codeCommit,
|
|
549
|
+
evaluatedRevision: pass.evaluatedRevision,
|
|
550
|
+
...(context.irNumber === undefined ? {} : { irNumber: context.irNumber }),
|
|
551
|
+
...(context.irTask === undefined ? {} : { irTask: context.irTask }),
|
|
552
|
+
},
|
|
553
|
+
lastError: undefined,
|
|
554
|
+
};
|
|
524
555
|
}),
|
|
525
556
|
rememberReviewNotPassed: assign(({ event }) => {
|
|
526
557
|
const output = doneOutputOf(event);
|
|
@@ -703,6 +734,9 @@ export const codeMachine = setup({
|
|
|
703
734
|
? { irNumber: context.irNumber }
|
|
704
735
|
: {}),
|
|
705
736
|
runResults: context.runResults,
|
|
737
|
+
previousPhaseReview: context.previousPhaseReview === undefined
|
|
738
|
+
? 'No accepted prior-phase review is available.'
|
|
739
|
+
: JSON.stringify(context.previousPhaseReview),
|
|
706
740
|
...bossReplyFields(context),
|
|
707
741
|
}),
|
|
708
742
|
onDone: [
|
|
@@ -95,6 +95,8 @@ export interface IrTaskPhasePlayerInput extends PlayerInputBase {
|
|
|
95
95
|
readonly sourceItem: 'CODE-3';
|
|
96
96
|
/** `<ir-number>`: the IR whose next unfinished task this phase implements. */
|
|
97
97
|
readonly irNumber?: string;
|
|
98
|
+
/** Source-owned relay; historical canonical evidence, not present approval. */
|
|
99
|
+
readonly previousPhaseReview: string;
|
|
98
100
|
}
|
|
99
101
|
|
|
100
102
|
export type PlayerInput = FirstPhasePlayerInput | IrTaskPhasePlayerInput;
|
|
@@ -194,6 +196,15 @@ export interface CodeInput {
|
|
|
194
196
|
readonly runResults?: string;
|
|
195
197
|
}
|
|
196
198
|
|
|
199
|
+
export interface PreviousPhaseReview {
|
|
200
|
+
readonly phaseKind: 'direct' | 'new-intent' | 'ir-task';
|
|
201
|
+
readonly phaseOutcome: PhaseOutcome;
|
|
202
|
+
readonly scopeCommit: string;
|
|
203
|
+
readonly evaluatedRevision: string;
|
|
204
|
+
readonly irNumber?: string;
|
|
205
|
+
readonly irTask?: string;
|
|
206
|
+
}
|
|
207
|
+
|
|
197
208
|
export interface CodeContext {
|
|
198
209
|
/** `<run-results>`: relevant run results relayed to every phase. */
|
|
199
210
|
readonly runResults: string;
|
|
@@ -211,6 +222,8 @@ export interface CodeContext {
|
|
|
211
222
|
readonly phaseOutcome?: PhaseOutcome;
|
|
212
223
|
/** Revision evaluated by the latest passing review. */
|
|
213
224
|
readonly evaluatedRevision?: string;
|
|
225
|
+
/** Frozen only by an accepted canonical clean child REVIEW. */
|
|
226
|
+
readonly previousPhaseReview?: PreviousPhaseReview;
|
|
214
227
|
/** Compact failure CODE reports when review did not pass a phase. */
|
|
215
228
|
readonly reviewError?: CompactError;
|
|
216
229
|
/** Sanitized evidence of the review result that ended CODE. */
|
|
@@ -249,13 +262,19 @@ const FIRST_PHASE_PROMPT = [
|
|
|
249
262
|
'For an existing IR, read the identified IR and implement exactly its next unfinished task, including corresponding tests or specs if any.',
|
|
250
263
|
'Do not implement a later task in this phase.',
|
|
251
264
|
"Mark the IR's progress and deliverables when relevant.",
|
|
265
|
+
"After implementing an IR task, identify it and state whether any IR task still has unfinished Coder work; if so identify the next unfinished task, otherwise state this was the final IR task's Coder work.",
|
|
266
|
+
"Report pending independent REVIEW, owner acceptance and later delivery/workshop steps separately; they imply an unfinished IR task only when the IR assigns that work to a remaining task.",
|
|
267
|
+
"Report only what the IR and completed work establish, in ordinary prose without a required marker.",
|
|
252
268
|
'If the IR will be finished after this phase, double-check that all acceptance criteria are met.',
|
|
253
269
|
'',
|
|
254
270
|
'Consult @specs/map.md for relevant context and @specs/meta.md for spec requirements, if needed.',
|
|
255
271
|
'',
|
|
256
272
|
'Keep to the original intent and follow what it asks.',
|
|
257
273
|
'Do not re-run tests or builds whose inputs have not changed since any previous reported run.',
|
|
258
|
-
"
|
|
274
|
+
"If a required Boss answer prevents completion of the current phase or IR task, ask that question and return without committing.",
|
|
275
|
+
"Preserve permitted uncommitted work for the deferred continuation; do not create a partial or blocker-only commit to meet the one-commit requirement.",
|
|
276
|
+
"After the answer allows completion, make exactly one phase-owned commit before REVIEW.",
|
|
277
|
+
"When the phase is complete, make its minimal changes and then one new commit, following @specs/packages/git.md; never amend an existing commit.",
|
|
259
278
|
'Make the commit message explain concisely what changed and why, including relevant verification.',
|
|
260
279
|
'Identify every new commit you make.',
|
|
261
280
|
'Credit every AI that contributed to this commit: Coder <coder-llm>.',
|
|
@@ -268,11 +287,20 @@ const IR_TASK_PHASE_PROMPT = [
|
|
|
268
287
|
'Read the identified IR and implement exactly its next unfinished task, including corresponding tests or specs if any.',
|
|
269
288
|
'Do not implement a later task in this phase.',
|
|
270
289
|
"Mark the IR's progress and deliverables when relevant.",
|
|
290
|
+
"After implementing an IR task, identify it and state whether any IR task still has unfinished Coder work; if so identify the next unfinished task, otherwise state this was the final IR task's Coder work.",
|
|
291
|
+
"Report pending independent REVIEW, owner acceptance and later delivery/workshop steps separately; they imply an unfinished IR task only when the IR assigns that work to a remaining task.",
|
|
292
|
+
"Report only what the IR and completed work establish, in ordinary prose without a required marker.",
|
|
271
293
|
'If the IR will be finished after this phase, double-check that all acceptance criteria are met.',
|
|
294
|
+
'Use the quoted previous-phase review only for its exact recorded scope; committed pending-review text may predate that canonical result.',
|
|
295
|
+
'Before treating it as current review evidence, verify clean current HEAD equals its evaluatedRevision; a mismatch is not approval.',
|
|
296
|
+
'It does not replace independent review of this task or any new owner or release decision.',
|
|
272
297
|
'',
|
|
273
298
|
'Keep to the original intent and follow what it asks.',
|
|
274
299
|
'Do not re-run tests or builds whose inputs have not changed since any previous reported run.',
|
|
275
|
-
"
|
|
300
|
+
"If a required Boss answer prevents completion of the current phase or IR task, ask that question and return without committing.",
|
|
301
|
+
"Preserve permitted uncommitted work for the deferred continuation; do not create a partial or blocker-only commit to meet the one-commit requirement.",
|
|
302
|
+
"After the answer allows completion, make exactly one phase-owned commit before REVIEW.",
|
|
303
|
+
"When the phase is complete, make its minimal changes and then one new commit, following @specs/packages/git.md; never amend an existing commit.",
|
|
276
304
|
'Make the commit message explain concisely what changed and why, including relevant verification.',
|
|
277
305
|
'Identify every new commit you make.',
|
|
278
306
|
'Credit every AI that contributed to this commit: Coder <coder-llm>.',
|
|
@@ -280,6 +308,7 @@ const IR_TASK_PHASE_PROMPT = [
|
|
|
280
308
|
'> Original request: <caller-input>',
|
|
281
309
|
'> IR number: <ir-number>',
|
|
282
310
|
'> Run results: <run-results>',
|
|
311
|
+
'> Previous phase review: <previous-phase-review>',
|
|
283
312
|
].join('\n');
|
|
284
313
|
|
|
285
314
|
const REVIEW_NEW_INTENT_PHASE_TEMPLATE = [
|
|
@@ -297,7 +326,7 @@ const REVIEW_IR_TASK_PHASE_TEMPLATE = [
|
|
|
297
326
|
].join('\n');
|
|
298
327
|
|
|
299
328
|
const NEEDS_BOSS_REPLY_DESCRIPTION =
|
|
300
|
-
"
|
|
329
|
+
"At the end of this call, Coder has an unanswered clarifying question for Boss whose answer is required to complete the current coding phase. Earlier interim questions resolved or superseded by later affirmative current-phase completion, and prerequisites only for later review, acceptance or release stages, do not establish this outcome. Output shall include `question: <verbatim question text from the acting agent's prose>`.";
|
|
301
330
|
|
|
302
331
|
const FIRST_PHASE_RESULT = {
|
|
303
332
|
directCommit:
|
|
@@ -823,6 +852,7 @@ export const codeMachine = setup({
|
|
|
823
852
|
irTask: undefined,
|
|
824
853
|
phaseOutcome: undefined,
|
|
825
854
|
evaluatedRevision: undefined,
|
|
855
|
+
previousPhaseReview: undefined,
|
|
826
856
|
reviewError: undefined,
|
|
827
857
|
reviewEvidence: undefined,
|
|
828
858
|
lastError: undefined,
|
|
@@ -834,6 +864,7 @@ export const codeMachine = setup({
|
|
|
834
864
|
resetForInterrupt: assign({
|
|
835
865
|
phaseOutcome: undefined,
|
|
836
866
|
evaluatedRevision: undefined,
|
|
867
|
+
previousPhaseReview: undefined,
|
|
837
868
|
reviewError: undefined,
|
|
838
869
|
reviewEvidence: undefined,
|
|
839
870
|
lastError: undefined,
|
|
@@ -876,11 +907,24 @@ export const codeMachine = setup({
|
|
|
876
907
|
pendingBossQuestion: undefined,
|
|
877
908
|
bossReply: undefined,
|
|
878
909
|
}),
|
|
879
|
-
rememberReviewPass: assign(({ event }) => {
|
|
910
|
+
rememberReviewPass: assign(({ context, event }) => {
|
|
880
911
|
const pass = reviewPassOf(event);
|
|
881
|
-
return pass === undefined
|
|
912
|
+
return pass === undefined || context.codeCommit === undefined || context.phaseOutcome === undefined
|
|
882
913
|
? {}
|
|
883
|
-
: {
|
|
914
|
+
: {
|
|
915
|
+
evaluatedRevision: pass.evaluatedRevision,
|
|
916
|
+
previousPhaseReview: {
|
|
917
|
+
phaseKind: context.phaseOutcome === 'directCommit'
|
|
918
|
+
? 'direct' as const : context.phaseOutcome === 'irCommit'
|
|
919
|
+
? 'new-intent' as const : 'ir-task' as const,
|
|
920
|
+
phaseOutcome: context.phaseOutcome,
|
|
921
|
+
scopeCommit: context.codeCommit,
|
|
922
|
+
evaluatedRevision: pass.evaluatedRevision,
|
|
923
|
+
...(context.irNumber === undefined ? {} : { irNumber: context.irNumber }),
|
|
924
|
+
...(context.irTask === undefined ? {} : { irTask: context.irTask }),
|
|
925
|
+
},
|
|
926
|
+
lastError: undefined,
|
|
927
|
+
};
|
|
884
928
|
}),
|
|
885
929
|
rememberReviewNotPassed: assign(({ event }) => {
|
|
886
930
|
const output = doneOutputOf(event);
|
|
@@ -1086,6 +1130,9 @@ export const codeMachine = setup({
|
|
|
1086
1130
|
? { irNumber: context.irNumber }
|
|
1087
1131
|
: {}),
|
|
1088
1132
|
runResults: context.runResults,
|
|
1133
|
+
previousPhaseReview: context.previousPhaseReview === undefined
|
|
1134
|
+
? 'No accepted prior-phase review is available.'
|
|
1135
|
+
: JSON.stringify(context.previousPhaseReview),
|
|
1089
1136
|
...bossReplyFields(context),
|
|
1090
1137
|
}),
|
|
1091
1138
|
onDone: [
|