@jkwd/inbase 0.1.4 → 0.1.6
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 +6 -1
- package/apps/explorer/scripts/patch-lib.d.ts +13 -0
- package/apps/explorer/scripts/patch-lib.mjs +162 -25
- package/apps/explorer/scripts/session-store.d.ts +37 -0
- package/apps/explorer/scripts/session-store.mjs +415 -53
- package/apps/explorer/src/App.tsx +307 -88
- package/apps/explorer/src/agentIntent.ts +42 -1
- package/apps/explorer/src/index.css +312 -5
- package/apps/explorer/src/layout.ts +111 -1
- package/apps/explorer/src/scene/Bridge.tsx +23 -5
- package/apps/explorer/src/scene/FileBlock.tsx +113 -144
- package/apps/explorer/src/scene/FolderArea.tsx +35 -16
- package/apps/explorer/src/scene/MapSelectBorder.tsx +3 -1
- package/apps/explorer/src/scene/MapView.tsx +77 -22
- package/apps/explorer/src/scene/Player.tsx +36 -1
- package/apps/explorer/src/scene/RelationLines.tsx +36 -32
- package/apps/explorer/src/scene/SelectionController.tsx +57 -16
- package/apps/explorer/src/scene/SelectionThumbnail.tsx +895 -0
- package/apps/explorer/src/scene/World.tsx +42 -44
- package/apps/explorer/src/theme.ts +17 -4
- package/apps/explorer/src/types.ts +17 -0
- package/apps/explorer/src/ui/HUD.tsx +946 -385
- package/apps/explorer/vite.config.ts +44 -22
- package/bin/session.mjs +34 -8
- package/package.json +1 -1
- package/skill/inbase/SKILL.md +35 -22
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import fs from 'node:fs'
|
|
2
|
-
import os from 'node:os'
|
|
3
2
|
import path from 'node:path'
|
|
3
|
+
import { spawnSync } from 'node:child_process'
|
|
4
4
|
import {
|
|
5
5
|
accumulatePatchAdditions,
|
|
6
6
|
applyUnifiedPatch,
|
|
7
|
+
applyUnifiedPatchToContents,
|
|
7
8
|
collectCreateFolders,
|
|
8
9
|
extractPatchImports,
|
|
9
10
|
foldersFromFileIds,
|
|
@@ -11,6 +12,8 @@ import {
|
|
|
11
12
|
} from './patch-lib.mjs'
|
|
12
13
|
|
|
13
14
|
const SESSION_ID = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/
|
|
15
|
+
const CONNECTED_TTL_MS = 15_000
|
|
16
|
+
const STALLED_WAIT_MS = 2_000
|
|
14
17
|
|
|
15
18
|
export function assertSessionId(value) {
|
|
16
19
|
if (typeof value !== 'string' || !SESSION_ID.test(value) || value === '.' || value === '..') {
|
|
@@ -46,9 +49,48 @@ export function sessionPaths(dataDir, sessionId) {
|
|
|
46
49
|
blueprint: path.join(root, 'blueprint.json'),
|
|
47
50
|
baseline: path.join(root, 'baseline.json'),
|
|
48
51
|
baselineFiles: path.join(root, 'baseline'),
|
|
52
|
+
stopped: path.join(dataDir, 'diff-sessions', `${safeId}.stopped`),
|
|
49
53
|
}
|
|
50
54
|
}
|
|
51
55
|
|
|
56
|
+
export function sessionStoppedError(sessionId) {
|
|
57
|
+
return new Error(
|
|
58
|
+
`VISUAL_CODER_STOPPED Session ${assertSessionId(sessionId)} was stopped. Do not modify project files.`,
|
|
59
|
+
)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function isSessionStopped(dataDir, sessionId) {
|
|
63
|
+
return fs.existsSync(sessionPaths(dataDir, sessionId).stopped)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function isWorkflowStopped(dataDir, sessionId) {
|
|
67
|
+
const safeId = assertSessionId(sessionId)
|
|
68
|
+
const manifest = readManifest(dataDir, safeId)
|
|
69
|
+
if (manifest?.phase === 'stopped') return true
|
|
70
|
+
return !manifest && isSessionStopped(dataDir, safeId)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function writeStoppedMarker(dataDir, sessionId) {
|
|
74
|
+
const { stopped } = sessionPaths(dataDir, sessionId)
|
|
75
|
+
atomicWrite(
|
|
76
|
+
stopped,
|
|
77
|
+
`${JSON.stringify({ sessionId: assertSessionId(sessionId), stoppedAt: new Date().toISOString() }, null, 2)}\n`,
|
|
78
|
+
)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function clearStoppedMarker(dataDir, sessionId) {
|
|
82
|
+
const { stopped } = sessionPaths(dataDir, sessionId)
|
|
83
|
+
if (fs.existsSync(stopped)) fs.unlinkSync(stopped)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function requireManifest(dataDir, sessionId, missingMessage) {
|
|
87
|
+
const safeId = assertSessionId(sessionId)
|
|
88
|
+
const manifest = readManifest(dataDir, safeId)
|
|
89
|
+
if (manifest) return manifest
|
|
90
|
+
if (isSessionStopped(dataDir, safeId)) throw sessionStoppedError(safeId)
|
|
91
|
+
throw new Error(missingMessage ?? `Unknown session ${safeId}`)
|
|
92
|
+
}
|
|
93
|
+
|
|
52
94
|
export function resolveTargetFile(targetRoot, fileId) {
|
|
53
95
|
if (typeof fileId !== 'string' || fileId.trim() === '') {
|
|
54
96
|
throw new Error('fileId is required')
|
|
@@ -78,6 +120,147 @@ export function writeActiveSession(dataDir, sessionId) {
|
|
|
78
120
|
)
|
|
79
121
|
}
|
|
80
122
|
|
|
123
|
+
function connectionFile(dataDir, sessionId) {
|
|
124
|
+
return path.join(sessionPaths(dataDir, sessionId).root, 'connected.json')
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function isFreshTimestamp(value, now = Date.now()) {
|
|
128
|
+
if (typeof value !== 'string') return false
|
|
129
|
+
const at = Date.parse(value)
|
|
130
|
+
return Number.isFinite(at) && now - at >= 0 && now - at < CONNECTED_TTL_MS
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export function touchSessionConnection(dataDir, sessionId) {
|
|
134
|
+
const safeId = assertSessionId(sessionId)
|
|
135
|
+
if (isSessionStopped(dataDir, safeId)) return
|
|
136
|
+
atomicWrite(
|
|
137
|
+
connectionFile(dataDir, safeId),
|
|
138
|
+
`${JSON.stringify({ sessionId: safeId, connectedAt: new Date().toISOString() }, null, 2)}\n`,
|
|
139
|
+
)
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function waiterSessionIds() {
|
|
143
|
+
try {
|
|
144
|
+
const result = spawnSync('ps', ['-ax', '-o', 'command='], {
|
|
145
|
+
encoding: 'utf8',
|
|
146
|
+
})
|
|
147
|
+
if (result.status !== 0 || !result.stdout) return new Set()
|
|
148
|
+
const ids = new Set()
|
|
149
|
+
for (const line of result.stdout.split('\n')) {
|
|
150
|
+
if (
|
|
151
|
+
!line.includes('wait-for-blueprint') &&
|
|
152
|
+
!line.includes('wait-for-approval')
|
|
153
|
+
) {
|
|
154
|
+
continue
|
|
155
|
+
}
|
|
156
|
+
const match = line.match(/--session\s+(\S+)/)
|
|
157
|
+
if (!match) continue
|
|
158
|
+
try {
|
|
159
|
+
ids.add(assertSessionId(match[1]))
|
|
160
|
+
} catch {
|
|
161
|
+
// Ignore process command lines with invalid session ids.
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
return ids
|
|
165
|
+
} catch {
|
|
166
|
+
return new Set()
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function isGeneratingPhase(phase) {
|
|
171
|
+
return phase === 'preparing' || phase === 'working' || phase === 'replanning'
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function isStalledWorking(manifest, waiterIds, sessionId, now = Date.now()) {
|
|
175
|
+
if (manifest.phase !== 'working') return false
|
|
176
|
+
if (!waiterIds.has(sessionId)) return false
|
|
177
|
+
const started = Date.parse(manifest.workStartedAt)
|
|
178
|
+
return Number.isFinite(started) && now - started >= STALLED_WAIT_MS
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export function isSessionConnected(
|
|
182
|
+
dataDir,
|
|
183
|
+
sessionId,
|
|
184
|
+
waiterIds = waiterSessionIds(),
|
|
185
|
+
) {
|
|
186
|
+
const safeId = assertSessionId(sessionId)
|
|
187
|
+
const manifest = readManifest(dataDir, safeId)
|
|
188
|
+
if (!manifest) return false
|
|
189
|
+
if (
|
|
190
|
+
manifest.phase === 'finished' ||
|
|
191
|
+
manifest.phase === 'stopped' ||
|
|
192
|
+
manifest.status === 'finished' ||
|
|
193
|
+
manifest.status === 'rejected'
|
|
194
|
+
) {
|
|
195
|
+
return false
|
|
196
|
+
}
|
|
197
|
+
if (isGeneratingPhase(manifest.phase)) return true
|
|
198
|
+
if (waiterIds.has(safeId)) return true
|
|
199
|
+
const connected = readJson(connectionFile(dataDir, safeId), null)
|
|
200
|
+
if (isFreshTimestamp(connected?.connectedAt)) return true
|
|
201
|
+
return isFreshTimestamp(manifest.updatedAt) || isFreshTimestamp(manifest.createdAt)
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function diffSessionsRoot(dataDir) {
|
|
205
|
+
return path.join(dataDir, 'diff-sessions')
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
export function listStoredSessionIds(dataDir) {
|
|
209
|
+
const root = diffSessionsRoot(dataDir)
|
|
210
|
+
if (!fs.existsSync(root)) return []
|
|
211
|
+
const ids = new Set()
|
|
212
|
+
for (const entry of fs.readdirSync(root, { withFileTypes: true })) {
|
|
213
|
+
if (entry.name === '.gitkeep') continue
|
|
214
|
+
const name =
|
|
215
|
+
entry.isFile() && entry.name.endsWith('.stopped')
|
|
216
|
+
? entry.name.slice(0, -'.stopped'.length)
|
|
217
|
+
: entry.name
|
|
218
|
+
try {
|
|
219
|
+
ids.add(assertSessionId(name))
|
|
220
|
+
} catch {
|
|
221
|
+
// Skip files that are not valid session ids.
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
return [...ids]
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
export function listOpenSessionIds(dataDir, waiterIds = waiterSessionIds()) {
|
|
228
|
+
const root = diffSessionsRoot(dataDir)
|
|
229
|
+
if (!fs.existsSync(root)) return []
|
|
230
|
+
const sessions = []
|
|
231
|
+
for (const entry of fs.readdirSync(root, { withFileTypes: true })) {
|
|
232
|
+
if (!entry.isDirectory()) continue
|
|
233
|
+
try {
|
|
234
|
+
const sessionId = assertSessionId(entry.name)
|
|
235
|
+
if (!isSessionConnected(dataDir, sessionId, waiterIds)) continue
|
|
236
|
+
const manifest = readManifest(dataDir, sessionId)
|
|
237
|
+
if (!manifest) continue
|
|
238
|
+
sessions.push({
|
|
239
|
+
sessionId,
|
|
240
|
+
createdAt: typeof manifest.createdAt === 'string' ? manifest.createdAt : '',
|
|
241
|
+
})
|
|
242
|
+
} catch {
|
|
243
|
+
// Skip folders that are not valid session ids.
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
sessions.sort((left, right) => {
|
|
247
|
+
if (left.createdAt !== right.createdAt) {
|
|
248
|
+
return left.createdAt.localeCompare(right.createdAt)
|
|
249
|
+
}
|
|
250
|
+
return left.sessionId.localeCompare(right.sessionId)
|
|
251
|
+
})
|
|
252
|
+
return sessions.map((item) => item.sessionId)
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
export function listSessionIntents(dataDir, knownFileIds = []) {
|
|
256
|
+
const waiters = waiterSessionIds()
|
|
257
|
+
return listOpenSessionIds(dataDir, waiters)
|
|
258
|
+
.map((sessionId) =>
|
|
259
|
+
sessionIntent(dataDir, sessionId, knownFileIds, undefined, waiters),
|
|
260
|
+
)
|
|
261
|
+
.filter(Boolean)
|
|
262
|
+
}
|
|
263
|
+
|
|
81
264
|
export function readBlueprintSession(dataDir) {
|
|
82
265
|
const value = readJson(path.join(dataDir, 'blueprint-session.json'), null)
|
|
83
266
|
return value?.sessionId ? assertSessionId(value.sessionId) : null
|
|
@@ -140,6 +323,7 @@ export function readManifest(dataDir, sessionId) {
|
|
|
140
323
|
value.pendingInstruction ??= null
|
|
141
324
|
value.workStartedAt ??= null
|
|
142
325
|
}
|
|
326
|
+
if (typeof value.stepByStep !== 'boolean') value.stepByStep = true
|
|
143
327
|
return value
|
|
144
328
|
}
|
|
145
329
|
|
|
@@ -244,7 +428,13 @@ export function previewPatchChain(patches, knownFileIds = []) {
|
|
|
244
428
|
}
|
|
245
429
|
}
|
|
246
430
|
|
|
247
|
-
export function sessionIntent(
|
|
431
|
+
export function sessionIntent(
|
|
432
|
+
dataDir,
|
|
433
|
+
sessionId,
|
|
434
|
+
knownFileIds = [],
|
|
435
|
+
selectedDiffId,
|
|
436
|
+
waiterIds = waiterSessionIds(),
|
|
437
|
+
) {
|
|
248
438
|
const manifest = readManifest(dataDir, sessionId)
|
|
249
439
|
if (!manifest) return null
|
|
250
440
|
const selectedId = selectedDiffId || manifest.activeDiffId
|
|
@@ -294,6 +484,7 @@ export function sessionIntent(dataDir, sessionId, knownFileIds = [], selectedDif
|
|
|
294
484
|
feature: manifest.feature,
|
|
295
485
|
steps: manifest.steps,
|
|
296
486
|
step: activeView ? manifest.currentStep : selected?.step ?? manifest.currentStep,
|
|
487
|
+
stepByStep: isStepByStep(manifest),
|
|
297
488
|
reason: activeView ? currentPlanStep?.title ?? null : selected?.title ?? null,
|
|
298
489
|
sessionId,
|
|
299
490
|
diffId: selected?.id ?? null,
|
|
@@ -312,6 +503,7 @@ export function sessionIntent(dataDir, sessionId, knownFileIds = [], selectedDif
|
|
|
312
503
|
manifest.phase === 'preparing' ||
|
|
313
504
|
manifest.phase === 'working' ||
|
|
314
505
|
manifest.phase === 'replanning',
|
|
506
|
+
stalledWait: isStalledWorking(manifest, waiterIds, sessionId),
|
|
315
507
|
creationMode: manifest.phase === 'blueprint' && ownsBlueprintLock,
|
|
316
508
|
canEnterBlueprint,
|
|
317
509
|
blueprintSessionId,
|
|
@@ -400,8 +592,51 @@ function replayPatches(dataDir, sessionId, targetRoot, entries) {
|
|
|
400
592
|
}
|
|
401
593
|
}
|
|
402
594
|
|
|
403
|
-
function
|
|
404
|
-
|
|
595
|
+
function gitTopLevel(fromDir) {
|
|
596
|
+
try {
|
|
597
|
+
const result = spawnSync('git', ['rev-parse', '--show-toplevel'], {
|
|
598
|
+
cwd: fromDir,
|
|
599
|
+
encoding: 'utf8',
|
|
600
|
+
})
|
|
601
|
+
if (result.status !== 0) return null
|
|
602
|
+
const root = result.stdout.trim()
|
|
603
|
+
return root ? fs.realpathSync(root) : null
|
|
604
|
+
} catch {
|
|
605
|
+
return null
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
function repoRelativePath(root, absolutePath) {
|
|
610
|
+
const resolved = path.resolve(absolutePath)
|
|
611
|
+
let candidate = resolved
|
|
612
|
+
try {
|
|
613
|
+
if (fs.existsSync(resolved)) candidate = fs.realpathSync(resolved)
|
|
614
|
+
else if (fs.existsSync(path.dirname(resolved))) {
|
|
615
|
+
candidate = path.join(fs.realpathSync(path.dirname(resolved)), path.basename(resolved))
|
|
616
|
+
}
|
|
617
|
+
} catch {
|
|
618
|
+
candidate = resolved
|
|
619
|
+
}
|
|
620
|
+
const relative = path.relative(root, candidate)
|
|
621
|
+
if (!relative || relative.startsWith('..') || path.isAbsolute(relative)) return null
|
|
622
|
+
return relative
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
function unstagePaths(fromDir, absolutePaths) {
|
|
626
|
+
if (!absolutePaths.length) return
|
|
627
|
+
const root = gitTopLevel(fromDir)
|
|
628
|
+
if (!root) return
|
|
629
|
+
const relative = [...new Set(absolutePaths)]
|
|
630
|
+
.map((item) => repoRelativePath(root, item))
|
|
631
|
+
.filter((item) => Boolean(item))
|
|
632
|
+
if (!relative.length) return
|
|
633
|
+
for (const item of relative) {
|
|
634
|
+
spawnSync('git', ['restore', '--staged', '--', item], {
|
|
635
|
+
cwd: root,
|
|
636
|
+
encoding: 'utf8',
|
|
637
|
+
stdio: 'ignore',
|
|
638
|
+
})
|
|
639
|
+
}
|
|
405
640
|
}
|
|
406
641
|
|
|
407
642
|
function liveEntries(manifest, diffId) {
|
|
@@ -409,8 +644,7 @@ function liveEntries(manifest, diffId) {
|
|
|
409
644
|
}
|
|
410
645
|
|
|
411
646
|
export function materializeDiff(dataDir, targetRoot, sessionId, diffId) {
|
|
412
|
-
const manifest =
|
|
413
|
-
if (!manifest) throw new Error(`Unknown session ${sessionId}`)
|
|
647
|
+
const manifest = requireManifest(dataDir, sessionId)
|
|
414
648
|
const through = diffId || manifest.activeDiffId
|
|
415
649
|
if (!through) return manifest
|
|
416
650
|
restoreBaseline(dataDir, sessionId, targetRoot)
|
|
@@ -418,25 +652,47 @@ export function materializeDiff(dataDir, targetRoot, sessionId, diffId) {
|
|
|
418
652
|
return manifest
|
|
419
653
|
}
|
|
420
654
|
|
|
421
|
-
function
|
|
422
|
-
const
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
for (const
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
fs.rmSync(temporary, { recursive: true, force: true })
|
|
655
|
+
function loadReplayContents(dataDir, sessionId, targetRoot, patches) {
|
|
656
|
+
const baseline = readBaseline(dataDir, sessionId)
|
|
657
|
+
const paths = sessionPaths(dataDir, sessionId)
|
|
658
|
+
const fileIds = new Set(Object.keys(baseline.files))
|
|
659
|
+
for (const patchText of patches) {
|
|
660
|
+
for (const entry of parseUnifiedPatch(patchText).entries) {
|
|
661
|
+
fileIds.add(entry.id)
|
|
662
|
+
}
|
|
430
663
|
}
|
|
664
|
+
|
|
665
|
+
const files = new Map()
|
|
666
|
+
for (const fileId of fileIds) {
|
|
667
|
+
const info = baseline.files[fileId]
|
|
668
|
+
if (info) {
|
|
669
|
+
if (!info.existed) continue
|
|
670
|
+
const stored = resolveTargetFile(paths.baselineFiles, fileId).absolute
|
|
671
|
+
if (fs.existsSync(stored) && fs.statSync(stored).isFile()) {
|
|
672
|
+
files.set(fileId, fs.readFileSync(stored, 'utf8'))
|
|
673
|
+
}
|
|
674
|
+
continue
|
|
675
|
+
}
|
|
676
|
+
const { absolute } = resolveTargetFile(targetRoot, fileId)
|
|
677
|
+
if (fs.existsSync(absolute) && fs.statSync(absolute).isFile()) {
|
|
678
|
+
files.set(fileId, fs.readFileSync(absolute, 'utf8'))
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
return files
|
|
431
682
|
}
|
|
432
683
|
|
|
433
684
|
export function validateContinuation(dataDir, manifest, targetRoot, patchText) {
|
|
434
685
|
const prior = manifest.diffs
|
|
435
686
|
.filter((entry) => entry.status !== 'rejected')
|
|
436
687
|
.map((entry) => readDiff(dataDir, manifest.sessionId, entry))
|
|
437
|
-
|
|
438
|
-
|
|
688
|
+
const patches = [...prior, patchText]
|
|
689
|
+
const files = loadReplayContents(
|
|
690
|
+
dataDir,
|
|
691
|
+
manifest.sessionId,
|
|
692
|
+
targetRoot,
|
|
693
|
+
patches,
|
|
439
694
|
)
|
|
695
|
+
for (const next of patches) applyUnifiedPatchToContents(files, next)
|
|
440
696
|
}
|
|
441
697
|
|
|
442
698
|
export function inspectTargetFile(
|
|
@@ -471,8 +727,36 @@ function featureName(value) {
|
|
|
471
727
|
return trimmed
|
|
472
728
|
}
|
|
473
729
|
|
|
730
|
+
export function isStepByStep(manifest) {
|
|
731
|
+
return manifest?.stepByStep !== false
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
export function autoAdvance(dataDir, sessionId, targetRoot = null) {
|
|
735
|
+
const manifest = readManifest(dataDir, sessionId)
|
|
736
|
+
if (!manifest || isStepByStep(manifest)) return manifest
|
|
737
|
+
if (manifest.phase === 'plan_ready') {
|
|
738
|
+
return invokeStep(dataDir, sessionId, manifest.currentStep, targetRoot)
|
|
739
|
+
}
|
|
740
|
+
if (manifest.phase === 'review') {
|
|
741
|
+
const active = manifest.diffs.at(-1)
|
|
742
|
+
if (!active || active.status !== 'pending') return manifest
|
|
743
|
+
if (active.step >= manifest.steps.length) return manifest
|
|
744
|
+
return invokeStep(dataDir, sessionId, active.step + 1, targetRoot)
|
|
745
|
+
}
|
|
746
|
+
return manifest
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
export function setStepByStep(dataDir, sessionId, enabled, targetRoot = null) {
|
|
750
|
+
const manifest = requireManifest(dataDir, sessionId)
|
|
751
|
+
manifest.stepByStep = Boolean(enabled)
|
|
752
|
+
writeManifest(dataDir, manifest)
|
|
753
|
+
if (isStepByStep(manifest)) return manifest
|
|
754
|
+
return autoAdvance(dataDir, sessionId, targetRoot)
|
|
755
|
+
}
|
|
756
|
+
|
|
474
757
|
export function startSession(dataDir, input) {
|
|
475
758
|
const sessionId = assertSessionId(input.sessionId)
|
|
759
|
+
clearStoppedMarker(dataDir, sessionId)
|
|
476
760
|
const existing = readManifest(dataDir, sessionId)
|
|
477
761
|
if (existing) {
|
|
478
762
|
focusSession(dataDir, sessionId)
|
|
@@ -487,6 +771,7 @@ export function startSession(dataDir, input) {
|
|
|
487
771
|
steps: [],
|
|
488
772
|
status: 'active',
|
|
489
773
|
phase: 'blueprint_ask',
|
|
774
|
+
stepByStep: true,
|
|
490
775
|
currentStep: 1,
|
|
491
776
|
activeDiffId: null,
|
|
492
777
|
pendingInstruction: null,
|
|
@@ -502,8 +787,7 @@ export function startSession(dataDir, input) {
|
|
|
502
787
|
}
|
|
503
788
|
|
|
504
789
|
export function answerBlueprint(dataDir, sessionId, enabled) {
|
|
505
|
-
const manifest =
|
|
506
|
-
if (!manifest) throw new Error(`Unknown session ${sessionId}`)
|
|
790
|
+
const manifest = requireManifest(dataDir, sessionId)
|
|
507
791
|
if (manifest.phase !== 'blueprint_ask') {
|
|
508
792
|
throw new Error(`Session ${sessionId} is not asking for a blueprint`)
|
|
509
793
|
}
|
|
@@ -522,8 +806,7 @@ export function answerBlueprint(dataDir, sessionId, enabled) {
|
|
|
522
806
|
|
|
523
807
|
export function updateBlueprint(dataDir, sessionId, input = {}) {
|
|
524
808
|
const safeId = assertSessionId(sessionId)
|
|
525
|
-
const manifest =
|
|
526
|
-
if (!manifest) throw new Error(`Unknown session ${safeId}`)
|
|
809
|
+
const manifest = requireManifest(dataDir, safeId)
|
|
527
810
|
if (manifest.phase !== 'blueprint') {
|
|
528
811
|
throw new Error(`Session ${safeId} is not in blueprint mode`)
|
|
529
812
|
}
|
|
@@ -544,8 +827,7 @@ export function updateBlueprint(dataDir, sessionId, input = {}) {
|
|
|
544
827
|
|
|
545
828
|
export function sendBlueprint(dataDir, sessionId, input = {}) {
|
|
546
829
|
const safeId = assertSessionId(sessionId)
|
|
547
|
-
const manifest =
|
|
548
|
-
if (!manifest) throw new Error(`Unknown session ${safeId}`)
|
|
830
|
+
const manifest = requireManifest(dataDir, safeId)
|
|
549
831
|
if (manifest.phase !== 'blueprint') {
|
|
550
832
|
throw new Error(`Session ${safeId} is not in blueprint mode`)
|
|
551
833
|
}
|
|
@@ -570,6 +852,9 @@ export function sendBlueprint(dataDir, sessionId, input = {}) {
|
|
|
570
852
|
export function reportPlan(dataDir, input) {
|
|
571
853
|
const sessionId = assertSessionId(input.sessionId)
|
|
572
854
|
const existing = readManifest(dataDir, sessionId)
|
|
855
|
+
if (!existing && isSessionStopped(dataDir, sessionId)) {
|
|
856
|
+
throw sessionStoppedError(sessionId)
|
|
857
|
+
}
|
|
573
858
|
const now = new Date().toISOString()
|
|
574
859
|
|
|
575
860
|
if (existing?.phase === 'blueprint_ask' || existing?.phase === 'blueprint') {
|
|
@@ -586,6 +871,7 @@ export function reportPlan(dataDir, input) {
|
|
|
586
871
|
steps: [],
|
|
587
872
|
status: 'active',
|
|
588
873
|
phase: 'preparing',
|
|
874
|
+
stepByStep: true,
|
|
589
875
|
currentStep: 1,
|
|
590
876
|
activeDiffId: null,
|
|
591
877
|
pendingInstruction: null,
|
|
@@ -602,7 +888,7 @@ export function reportPlan(dataDir, input) {
|
|
|
602
888
|
manifest.workStartedAt = null
|
|
603
889
|
writeManifest(dataDir, manifest)
|
|
604
890
|
focusSession(dataDir, sessionId)
|
|
605
|
-
return
|
|
891
|
+
return autoAdvance(dataDir, sessionId)
|
|
606
892
|
}
|
|
607
893
|
|
|
608
894
|
if (existing.phase !== 'replanning') {
|
|
@@ -619,12 +905,11 @@ export function reportPlan(dataDir, input) {
|
|
|
619
905
|
existing.workStartedAt = null
|
|
620
906
|
writeManifest(dataDir, existing)
|
|
621
907
|
focusSession(dataDir, sessionId)
|
|
622
|
-
return
|
|
908
|
+
return autoAdvance(dataDir, sessionId)
|
|
623
909
|
}
|
|
624
910
|
|
|
625
911
|
export function invokeStep(dataDir, sessionId, step, targetRoot = null) {
|
|
626
|
-
const manifest =
|
|
627
|
-
if (!manifest) throw new Error(`Unknown session ${sessionId}`)
|
|
912
|
+
const manifest = requireManifest(dataDir, sessionId)
|
|
628
913
|
|
|
629
914
|
if (manifest.phase === 'review') {
|
|
630
915
|
if (!targetRoot) throw new Error('A target root is required to apply the current step')
|
|
@@ -665,8 +950,11 @@ export function invokeStep(dataDir, sessionId, step, targetRoot = null) {
|
|
|
665
950
|
|
|
666
951
|
export function appendDiff(dataDir, targetRoot, input) {
|
|
667
952
|
const sessionId = assertSessionId(input.sessionId)
|
|
668
|
-
const manifest =
|
|
669
|
-
|
|
953
|
+
const manifest = requireManifest(
|
|
954
|
+
dataDir,
|
|
955
|
+
sessionId,
|
|
956
|
+
`Report a plan for session ${sessionId} first`,
|
|
957
|
+
)
|
|
670
958
|
if (manifest.phase !== 'working') {
|
|
671
959
|
throw new Error(`Step ${manifest.currentStep} has not been invoked`)
|
|
672
960
|
}
|
|
@@ -721,7 +1009,14 @@ export function appendDiff(dataDir, targetRoot, input) {
|
|
|
721
1009
|
writeManifest(dataDir, manifest)
|
|
722
1010
|
materializeDiff(dataDir, targetRoot, sessionId, id)
|
|
723
1011
|
focusSession(dataDir, sessionId)
|
|
724
|
-
|
|
1012
|
+
const advanced = autoAdvance(dataDir, sessionId, targetRoot)
|
|
1013
|
+
if (!advanced) {
|
|
1014
|
+
throw new Error(`Session ${sessionId} disappeared after publishing a diff`)
|
|
1015
|
+
}
|
|
1016
|
+
return {
|
|
1017
|
+
manifest: advanced,
|
|
1018
|
+
entry: advanced.diffs.find((item) => item.id === id) ?? entry,
|
|
1019
|
+
}
|
|
725
1020
|
}
|
|
726
1021
|
|
|
727
1022
|
function pendingActive(manifest, diffId) {
|
|
@@ -748,8 +1043,7 @@ function applyUnresolved(dataDir, targetRoot, manifest, diffId) {
|
|
|
748
1043
|
}
|
|
749
1044
|
|
|
750
1045
|
export function continueDiff(dataDir, targetRoot, sessionId, diffId) {
|
|
751
|
-
const manifest =
|
|
752
|
-
if (!manifest) throw new Error(`Unknown session ${sessionId}`)
|
|
1046
|
+
const manifest = requireManifest(dataDir, sessionId)
|
|
753
1047
|
const active = pendingActive(manifest, diffId)
|
|
754
1048
|
applyUnresolved(dataDir, targetRoot, manifest, diffId)
|
|
755
1049
|
|
|
@@ -759,18 +1053,16 @@ export function continueDiff(dataDir, targetRoot, sessionId, diffId) {
|
|
|
759
1053
|
writeManifest(dataDir, manifest)
|
|
760
1054
|
finalizeFinishedSession(dataDir, sessionId)
|
|
761
1055
|
return manifest
|
|
762
|
-
} else {
|
|
763
|
-
manifest.currentStep = active.step + 1
|
|
764
|
-
manifest.phase = 'plan_ready'
|
|
765
|
-
manifest.workStartedAt = null
|
|
766
1056
|
}
|
|
1057
|
+
manifest.currentStep = active.step + 1
|
|
1058
|
+
manifest.phase = 'plan_ready'
|
|
1059
|
+
manifest.workStartedAt = null
|
|
767
1060
|
writeManifest(dataDir, manifest)
|
|
768
|
-
return
|
|
1061
|
+
return autoAdvance(dataDir, sessionId, targetRoot)
|
|
769
1062
|
}
|
|
770
1063
|
|
|
771
1064
|
export function requestReplan(dataDir, sessionId, diffId, instruction) {
|
|
772
|
-
const manifest =
|
|
773
|
-
if (!manifest) throw new Error(`Unknown session ${sessionId}`)
|
|
1065
|
+
const manifest = requireManifest(dataDir, sessionId)
|
|
774
1066
|
const active = pendingActive(manifest, diffId)
|
|
775
1067
|
const guidance = typeof instruction === 'string' ? instruction.trim() : ''
|
|
776
1068
|
if (!guidance) throw new Error('An alternative instruction is required')
|
|
@@ -784,14 +1076,91 @@ export function requestReplan(dataDir, sessionId, diffId, instruction) {
|
|
|
784
1076
|
return manifest
|
|
785
1077
|
}
|
|
786
1078
|
|
|
787
|
-
|
|
1079
|
+
function unstageDiffSessionArtifacts(dataDir, targetRoot, extraPaths = []) {
|
|
1080
|
+
if (!targetRoot) return
|
|
1081
|
+
unstagePaths(targetRoot, [
|
|
1082
|
+
...extraPaths,
|
|
1083
|
+
diffSessionsRoot(dataDir),
|
|
1084
|
+
path.join(dataDir, 'active-session.json'),
|
|
1085
|
+
path.join(dataDir, 'blueprint-session.json'),
|
|
1086
|
+
])
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1089
|
+
function discardStoredSession(
|
|
1090
|
+
dataDir,
|
|
1091
|
+
sessionId,
|
|
1092
|
+
targetRoot = null,
|
|
1093
|
+
{ restore = true, keepStoppedMarker = false } = {},
|
|
1094
|
+
) {
|
|
788
1095
|
const safeId = assertSessionId(sessionId)
|
|
789
|
-
const
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
1096
|
+
const paths = sessionPaths(dataDir, safeId)
|
|
1097
|
+
const fileIds = Object.keys(readBaseline(dataDir, safeId).files)
|
|
1098
|
+
if (targetRoot && restore) {
|
|
1099
|
+
try {
|
|
1100
|
+
restoreBaseline(dataDir, safeId, targetRoot)
|
|
1101
|
+
} catch {
|
|
1102
|
+
// Incomplete session artifacts should still be deleted.
|
|
1103
|
+
}
|
|
793
1104
|
}
|
|
794
|
-
|
|
1105
|
+
if (targetRoot) {
|
|
1106
|
+
unstageDiffSessionArtifacts(
|
|
1107
|
+
dataDir,
|
|
1108
|
+
targetRoot,
|
|
1109
|
+
fileIds.flatMap((id) => {
|
|
1110
|
+
try {
|
|
1111
|
+
return [resolveTargetFile(targetRoot, id).absolute]
|
|
1112
|
+
} catch {
|
|
1113
|
+
return []
|
|
1114
|
+
}
|
|
1115
|
+
}),
|
|
1116
|
+
)
|
|
1117
|
+
}
|
|
1118
|
+
releaseBlueprintSession(dataDir, safeId)
|
|
1119
|
+
if (fs.existsSync(paths.root)) {
|
|
1120
|
+
fs.rmSync(paths.root, { recursive: true, force: true })
|
|
1121
|
+
}
|
|
1122
|
+
if (!keepStoppedMarker) clearStoppedMarker(dataDir, safeId)
|
|
1123
|
+
const active = readActiveSession(dataDir)
|
|
1124
|
+
if (active === safeId) writeActiveSession(dataDir, null)
|
|
1125
|
+
}
|
|
1126
|
+
|
|
1127
|
+
export function discardInactiveDiffSessions(
|
|
1128
|
+
dataDir,
|
|
1129
|
+
targetRoot = null,
|
|
1130
|
+
waiterIds = waiterSessionIds(),
|
|
1131
|
+
) {
|
|
1132
|
+
const keep = new Set()
|
|
1133
|
+
for (const value of waiterIds) {
|
|
1134
|
+
try {
|
|
1135
|
+
keep.add(assertSessionId(value))
|
|
1136
|
+
} catch {
|
|
1137
|
+
// Ignore process command lines with invalid session ids.
|
|
1138
|
+
}
|
|
1139
|
+
}
|
|
1140
|
+
|
|
1141
|
+
for (const sessionId of listStoredSessionIds(dataDir)) {
|
|
1142
|
+
const live = keep.has(sessionId) && Boolean(readManifest(dataDir, sessionId))
|
|
1143
|
+
const stopping = keep.has(sessionId) && isSessionStopped(dataDir, sessionId)
|
|
1144
|
+
if (live || stopping) continue
|
|
1145
|
+
discardStoredSession(dataDir, sessionId, targetRoot)
|
|
1146
|
+
}
|
|
1147
|
+
|
|
1148
|
+
const liveIds = [...keep].filter((id) => readManifest(dataDir, id))
|
|
1149
|
+
const active = readActiveSession(dataDir)
|
|
1150
|
+
if (active && !liveIds.includes(active)) writeActiveSession(dataDir, null)
|
|
1151
|
+
const locked = readBlueprintSession(dataDir)
|
|
1152
|
+
if (locked && !liveIds.includes(locked)) writeBlueprintSession(dataDir, null)
|
|
1153
|
+
unstageDiffSessionArtifacts(dataDir, targetRoot)
|
|
1154
|
+
return liveIds
|
|
1155
|
+
}
|
|
1156
|
+
|
|
1157
|
+
export function stopSession(dataDir, sessionId, targetRoot = null) {
|
|
1158
|
+
const safeId = assertSessionId(sessionId)
|
|
1159
|
+
writeStoppedMarker(dataDir, safeId)
|
|
1160
|
+
discardStoredSession(dataDir, safeId, targetRoot, { keepStoppedMarker: true })
|
|
1161
|
+
const waiters = waiterSessionIds()
|
|
1162
|
+
waiters.add(safeId)
|
|
1163
|
+
discardInactiveDiffSessions(dataDir, targetRoot, waiters)
|
|
795
1164
|
return null
|
|
796
1165
|
}
|
|
797
1166
|
|
|
@@ -819,14 +1188,7 @@ export function closeSession(dataDir, sessionId) {
|
|
|
819
1188
|
}
|
|
820
1189
|
|
|
821
1190
|
export function finalizeFinishedSession(dataDir, sessionId) {
|
|
822
|
-
|
|
823
|
-
const paths = sessionPaths(dataDir, safeId)
|
|
824
|
-
releaseBlueprintSession(dataDir, safeId)
|
|
825
|
-
if (fs.existsSync(paths.root)) {
|
|
826
|
-
fs.rmSync(paths.root, { recursive: true, force: true })
|
|
827
|
-
}
|
|
828
|
-
const active = readActiveSession(dataDir)
|
|
829
|
-
if (active === safeId) writeActiveSession(dataDir, null)
|
|
1191
|
+
discardStoredSession(dataDir, sessionId, null, { restore: false })
|
|
830
1192
|
}
|
|
831
1193
|
|
|
832
1194
|
export function emptyBlueprint() {
|