in2-agent-server 0.20260821.185429
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 +369 -0
- package/agents/plan-requirement.md +32 -0
- package/dist/campfire/configItem.js +201 -0
- package/dist/campfire/configItemDescription.js +379 -0
- package/dist/campfire/deriveFromBundle.js +1013 -0
- package/dist/cli.js +465 -0
- package/dist/core/betaFeedback.js +493 -0
- package/dist/core/chapterInstructions.js +473 -0
- package/dist/core/claudeAssets.js +61 -0
- package/dist/core/gate.js +201 -0
- package/dist/core/ids.js +9 -0
- package/dist/core/install.js +60 -0
- package/dist/core/operatorQa.js +462 -0
- package/dist/core/orgCallMeter.js +178 -0
- package/dist/core/parcels.js +1235 -0
- package/dist/core/paths.js +105 -0
- package/dist/core/phases.js +225 -0
- package/dist/core/planFile.js +251 -0
- package/dist/core/planFragments.js +119 -0
- package/dist/core/readsLog.js +398 -0
- package/dist/core/registration.js +24 -0
- package/dist/core/result.js +38 -0
- package/dist/core/resume.js +233 -0
- package/dist/core/revisionChain.js +351 -0
- package/dist/core/richText.js +61 -0
- package/dist/core/semanticHash.js +89 -0
- package/dist/core/sessionOrg.js +60 -0
- package/dist/core/sfQuery.js +19 -0
- package/dist/core/spawn.js +182 -0
- package/dist/core/supervisorContract.js +230 -0
- package/dist/core/telemetry.js +1297 -0
- package/dist/core/telemetryContext.js +154 -0
- package/dist/core/telemetrySpool.js +244 -0
- package/dist/core/transcriptUpload.js +516 -0
- package/dist/core/update.js +363 -0
- package/dist/core/viewer.js +117 -0
- package/dist/core/workspace.js +92 -0
- package/dist/core/writeGrants.js +93 -0
- package/dist/core/writebackManifest.js +92 -0
- package/dist/direct/applyLoop.js +2516 -0
- package/dist/direct/batchReport.js +510 -0
- package/dist/direct/comparatorNoise.js +75 -0
- package/dist/direct/containers.js +85 -0
- package/dist/direct/contamination.js +221 -0
- package/dist/direct/crud.js +56 -0
- package/dist/direct/destructive.js +168 -0
- package/dist/direct/knownTokens.js +94 -0
- package/dist/direct/lint.js +382 -0
- package/dist/direct/metadataSchema.generated.json +1 -0
- package/dist/direct/metadataSchema.js +179 -0
- package/dist/direct/operatorPage.js +349 -0
- package/dist/direct/orgRepo.js +1531 -0
- package/dist/direct/orgRepoSpawnCount.js +21 -0
- package/dist/direct/plannedScope.js +119 -0
- package/dist/direct/preImageTargets.js +63 -0
- package/dist/direct/refVerify.js +191 -0
- package/dist/direct/registry.js +129 -0
- package/dist/direct/removals.js +262 -0
- package/dist/direct/rollback.js +1513 -0
- package/dist/direct/rollbackRun.js +1360 -0
- package/dist/direct/sessionProjection.js +126 -0
- package/dist/direct/snapshotComparability.js +165 -0
- package/dist/direct/sourceTree.js +113 -0
- package/dist/direct/unstatedChanges.js +106 -0
- package/dist/direct/waveAssembly.js +556 -0
- package/dist/direct/waveCounts.js +19 -0
- package/dist/direct/waveDerivation.js +152 -0
- package/dist/direct/waveGrade.js +185 -0
- package/dist/index.js +90 -0
- package/dist/orgread/customFieldSnapshot.js +48 -0
- package/dist/orgread/fieldMetadata.js +178 -0
- package/dist/orgread/genericSnapshot.js +61 -0
- package/dist/orgread/layoutSnapshot.js +151 -0
- package/dist/orgread/salesforceOrgReader.js +184 -0
- package/dist/orgread/snapshotTypes.js +9 -0
- package/dist/projection/bridge.js +26 -0
- package/dist/projection/diff.js +141 -0
- package/dist/projection/mapmd.js +213 -0
- package/dist/projection/nameAmbiguity.js +106 -0
- package/dist/projection/precedent.js +136 -0
- package/dist/projection/project.js +214 -0
- package/dist/projection/scope.js +110 -0
- package/dist/projection/types.js +5 -0
- package/dist/tools/analyze.js +322 -0
- package/dist/tools/auth.js +915 -0
- package/dist/tools/campfire.js +1742 -0
- package/dist/tools/closeBatch.js +154 -0
- package/dist/tools/closeText.js +35 -0
- package/dist/tools/direct.js +1613 -0
- package/dist/tools/feedback.js +318 -0
- package/dist/tools/operator.js +287 -0
- package/dist/tools/org.js +739 -0
- package/dist/tools/phase.js +249 -0
- package/dist/tools/projection.js +172 -0
- package/dist/tools/session.js +313 -0
- package/dist/tools/update.js +82 -0
- package/dist/tools/waves.js +793 -0
- package/package.json +48 -0
- package/reference/campfire-configuration-items-guide.md +710 -0
- package/reference/deployability-cribs.md +119 -0
- package/reference/operator-voice.md +141 -0
- package/reference/org-read-cribs.md +34 -0
- package/reference/scoped-dml-interim.md +68 -0
- package/reference/supervisor-playbook.md +313 -0
- package/reference/templates.md +233 -0
- package/skills/in2-delivery/SKILL.md +322 -0
- package/skills/in2-delivery/references/campfire-configuration-items-guide.md +710 -0
- package/skills/in2-delivery/references/deployability-cribs.md +119 -0
- package/skills/in2-delivery/references/operator-voice.md +141 -0
- package/skills/in2-delivery/references/org-read-cribs.md +34 -0
- package/skills/in2-delivery/references/scoped-dml-interim.md +68 -0
- package/skills/in2-delivery/references/templates.md +233 -0
- package/skills/plan-requirement/SKILL.md +188 -0
- package/viewer/index.html +899 -0
- package/viewer/server.mjs +433 -0
|
@@ -0,0 +1,433 @@
|
|
|
1
|
+
// IN2 session viewer. A read-only localhost UI over one delivery session's on-disk artifacts,
|
|
2
|
+
// meant to be docked in the Claude Code desktop sidebar while a run executes.
|
|
3
|
+
//
|
|
4
|
+
// It writes NOTHING and talks to NOTHING: every fact on screen is a projection of files the
|
|
5
|
+
// cc-mcp-server already writes atomically (tmp+rename) under the session directory, so polling
|
|
6
|
+
// can never catch a torn write and the viewer can never desync the run. `activate_session` spawns
|
|
7
|
+
// this (src/core/viewer.ts) and returns the URL pinned to ?session=<id>; run by hand, it defaults
|
|
8
|
+
// to the most recently modified session dir.
|
|
9
|
+
//
|
|
10
|
+
// Run: node apps/cc-mcp-server/viewer/server.mjs (port 4199)
|
|
11
|
+
// Env: IN2_VIEWER_PORT, CC_MCP_WORKSPACE_ROOT (same var the server honors)
|
|
12
|
+
|
|
13
|
+
import { createServer } from "node:http";
|
|
14
|
+
import { readFileSync, readdirSync, existsSync, statSync } from "node:fs";
|
|
15
|
+
import { join, resolve } from "node:path";
|
|
16
|
+
import { homedir } from "node:os";
|
|
17
|
+
import { fileURLToPath } from "node:url";
|
|
18
|
+
|
|
19
|
+
const HERE = resolve(fileURLToPath(import.meta.url), "..");
|
|
20
|
+
const PORT = Number(process.env.IN2_VIEWER_PORT || 4199);
|
|
21
|
+
// argv[2] wins so a launch.json entry can point at a replay/scratch root without env plumbing.
|
|
22
|
+
const SESSIONS_ROOT =
|
|
23
|
+
process.argv[2] ||
|
|
24
|
+
process.env.CC_MCP_WORKSPACE_ROOT?.trim() ||
|
|
25
|
+
join(homedir(), ".in2-agent", ".local", "cc-mcp", "sessions");
|
|
26
|
+
|
|
27
|
+
// The nine chapters, mirrored from core/phases.ts. The viewer renders whatever the ledger says,
|
|
28
|
+
// so drift here only affects the "not yet reached" tail of the stepper.
|
|
29
|
+
const CHAPTERS = [
|
|
30
|
+
"activation",
|
|
31
|
+
"intake",
|
|
32
|
+
"partitioning",
|
|
33
|
+
"parallel-analysis",
|
|
34
|
+
"clarification",
|
|
35
|
+
"scope-approval",
|
|
36
|
+
"delivery",
|
|
37
|
+
"review-results",
|
|
38
|
+
"writeback-close"
|
|
39
|
+
];
|
|
40
|
+
const SKIPPABLE = new Set(["partitioning", "parallel-analysis", "clarification"]);
|
|
41
|
+
|
|
42
|
+
// ---------- safe readers ------------------------------------------------------
|
|
43
|
+
|
|
44
|
+
function readJson(file) {
|
|
45
|
+
try {
|
|
46
|
+
return JSON.parse(readFileSync(file, "utf8"));
|
|
47
|
+
} catch {
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function readText(file) {
|
|
53
|
+
try {
|
|
54
|
+
return readFileSync(file, "utf8");
|
|
55
|
+
} catch {
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function listDirs(dir) {
|
|
61
|
+
try {
|
|
62
|
+
return readdirSync(dir, { withFileTypes: true })
|
|
63
|
+
.filter((e) => e.isDirectory())
|
|
64
|
+
.map((e) => e.name);
|
|
65
|
+
} catch {
|
|
66
|
+
return [];
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function listFiles(dir) {
|
|
71
|
+
try {
|
|
72
|
+
return readdirSync(dir, { withFileTypes: true })
|
|
73
|
+
.filter((e) => e.isFile())
|
|
74
|
+
.map((e) => e.name);
|
|
75
|
+
} catch {
|
|
76
|
+
return [];
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Session ids are opaque `ses_<hex>`; refuse anything path-shaped.
|
|
81
|
+
const SESSION_ID_RE = /^ses_[a-z0-9]+$/;
|
|
82
|
+
|
|
83
|
+
function sessionDir(id) {
|
|
84
|
+
if (!SESSION_ID_RE.test(id)) return null;
|
|
85
|
+
const dir = join(SESSIONS_ROOT, id);
|
|
86
|
+
return existsSync(dir) ? dir : null;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function newestSessionId() {
|
|
90
|
+
let best = null;
|
|
91
|
+
let bestM = -1;
|
|
92
|
+
for (const name of listDirs(SESSIONS_ROOT)) {
|
|
93
|
+
if (!SESSION_ID_RE.test(name)) continue;
|
|
94
|
+
try {
|
|
95
|
+
const m = statSync(join(SESSIONS_ROOT, name)).mtimeMs;
|
|
96
|
+
if (m > bestM) {
|
|
97
|
+
bestM = m;
|
|
98
|
+
best = name;
|
|
99
|
+
}
|
|
100
|
+
} catch {
|
|
101
|
+
/* raced deletion */
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return best;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// ---------- state projection --------------------------------------------------
|
|
108
|
+
|
|
109
|
+
function projectPhases(dir) {
|
|
110
|
+
const raw = readText(join(dir, "phases.jsonl"));
|
|
111
|
+
const entries = [];
|
|
112
|
+
if (raw) {
|
|
113
|
+
for (const line of raw.split("\n")) {
|
|
114
|
+
if (!line.trim()) continue;
|
|
115
|
+
try {
|
|
116
|
+
entries.push(JSON.parse(line));
|
|
117
|
+
} catch {
|
|
118
|
+
/* partial line */
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
// Head = the chapter the run is currently IN (last `entering`); before any entry it's activation.
|
|
123
|
+
const head = entries.length ? entries[entries.length - 1].entering : "activation";
|
|
124
|
+
const completed = new Set(entries.map((e) => e.completed));
|
|
125
|
+
const chapters = CHAPTERS.map((name) => {
|
|
126
|
+
let status;
|
|
127
|
+
if (completed.has(name)) status = "done";
|
|
128
|
+
else if (name === head) status = "active";
|
|
129
|
+
else {
|
|
130
|
+
const headIdx = CHAPTERS.indexOf(head);
|
|
131
|
+
const idx = CHAPTERS.indexOf(name);
|
|
132
|
+
// A skippable chapter behind the head that never completed was skipped, not stalled.
|
|
133
|
+
status = idx < headIdx ? (SKIPPABLE.has(name) ? "skipped" : "done") : "pending";
|
|
134
|
+
}
|
|
135
|
+
const entry = entries.find((e) => e.completed === name);
|
|
136
|
+
return { name, status, at: entry?.at ?? null, note: entry?.note ?? null };
|
|
137
|
+
});
|
|
138
|
+
return { chapters, head, hasLedger: entries.length > 0 };
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function projectRequirements(dir, head) {
|
|
142
|
+
const index = readJson(join(dir, "requirements", "index.json")) || [];
|
|
143
|
+
const analysisUnderway =
|
|
144
|
+
CHAPTERS.indexOf(head) >= CHAPTERS.indexOf("partitioning") &&
|
|
145
|
+
CHAPTERS.indexOf(head) <= CHAPTERS.indexOf("clarification");
|
|
146
|
+
return index.map((r) => {
|
|
147
|
+
const reqDir = join(dir, "requirements", r.extId);
|
|
148
|
+
const plan = readJson(join(reqDir, "plan.json"));
|
|
149
|
+
let status, detail;
|
|
150
|
+
if (plan) {
|
|
151
|
+
// A resolved blocker is history, not a live block — only open ones color the row.
|
|
152
|
+
const openBlockers = (plan.blockers || []).filter((b) => !b.resolved);
|
|
153
|
+
status = openBlockers.length > 0 ? "blocked" : "analyzed";
|
|
154
|
+
detail = {
|
|
155
|
+
summary: plan.summary || null,
|
|
156
|
+
components: (plan.components || []).map((c) => ({
|
|
157
|
+
typeName: c.typeName,
|
|
158
|
+
fullName: c.fullName,
|
|
159
|
+
intent: c.intent,
|
|
160
|
+
status: c.status,
|
|
161
|
+
deployable: c.deployable !== false
|
|
162
|
+
})),
|
|
163
|
+
blockers: openBlockers,
|
|
164
|
+
assumptions: plan.assumptions || []
|
|
165
|
+
};
|
|
166
|
+
} else {
|
|
167
|
+
status = analysisUnderway ? "analyzing" : "queued";
|
|
168
|
+
detail = null;
|
|
169
|
+
}
|
|
170
|
+
return { extId: r.extId, title: r.title, status, detail };
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function projectQuestions(dir) {
|
|
175
|
+
const qdir = join(dir, "questions");
|
|
176
|
+
return listFiles(qdir)
|
|
177
|
+
.filter((f) => f.endsWith(".json"))
|
|
178
|
+
.sort()
|
|
179
|
+
.map((f) => {
|
|
180
|
+
const q = readJson(join(qdir, f));
|
|
181
|
+
if (!q) return null;
|
|
182
|
+
return {
|
|
183
|
+
round: q.round,
|
|
184
|
+
status: q.status,
|
|
185
|
+
askedAt: q.askedAt || null,
|
|
186
|
+
answeredAt: q.answeredAt || null,
|
|
187
|
+
questions: (q.questions || []).map((x) => ({
|
|
188
|
+
header: x.header,
|
|
189
|
+
question: x.question,
|
|
190
|
+
// answer: { chosenLabels: [...], answeredAt } once the operator has decided.
|
|
191
|
+
answer: x.answer ?? x.selected ?? null,
|
|
192
|
+
options: (x.options || []).map((o) => ({
|
|
193
|
+
label: o.label,
|
|
194
|
+
description: o.description ?? null
|
|
195
|
+
}))
|
|
196
|
+
}))
|
|
197
|
+
};
|
|
198
|
+
})
|
|
199
|
+
.filter(Boolean);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function projectWaves(dir) {
|
|
203
|
+
const ddir = join(dir, "direct");
|
|
204
|
+
const report = readJson(join(dir, "batch-report.json"));
|
|
205
|
+
const reportByWave = new Map((report?.waves || []).map((w) => [w.wave, w]));
|
|
206
|
+
return listDirs(ddir)
|
|
207
|
+
.filter((d) => !d.startsWith("teardown-"))
|
|
208
|
+
.sort()
|
|
209
|
+
.map((name) => {
|
|
210
|
+
const wdir = join(ddir, name);
|
|
211
|
+
const verdict = readJson(join(wdir, "verdict.json"));
|
|
212
|
+
const request = readJson(join(wdir, "request.json"));
|
|
213
|
+
const deploy = readJson(join(wdir, "deploy.json"));
|
|
214
|
+
const rep = reportByWave.get(name);
|
|
215
|
+
let status;
|
|
216
|
+
if (verdict) status = "done";
|
|
217
|
+
else if (deploy) status = "deploying";
|
|
218
|
+
else if (request) status = "in-progress";
|
|
219
|
+
else status = "assembling";
|
|
220
|
+
return {
|
|
221
|
+
name,
|
|
222
|
+
status,
|
|
223
|
+
verdict: verdict?.verdict ?? null,
|
|
224
|
+
deployStatus: verdict?.deploy?.status ?? deploy?.status ?? null,
|
|
225
|
+
dryRunStatus: verdict?.dryRun?.status ?? null,
|
|
226
|
+
lintBlocked: verdict?.lint?.blocked ?? null,
|
|
227
|
+
components: (request?.edits || []).map((e) => `${e.typeName}:${e.fullName}`),
|
|
228
|
+
requirementIds: rep?.requirementIds ?? [],
|
|
229
|
+
diff: verdict
|
|
230
|
+
? {
|
|
231
|
+
added: (verdict.diff?.added || []).length,
|
|
232
|
+
removed: (verdict.diff?.removed || []).length,
|
|
233
|
+
changed: (verdict.diff?.changed || []).length
|
|
234
|
+
}
|
|
235
|
+
: null,
|
|
236
|
+
failures: [
|
|
237
|
+
...(verdict?.dryRun?.failures || []),
|
|
238
|
+
...(verdict?.deploy?.failures || [])
|
|
239
|
+
].slice(0, 10)
|
|
240
|
+
};
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// The results rollup mirrors the original VFP batch-results page: per REQUIREMENT, not per wave.
|
|
245
|
+
// A requirement's outcome is derived from the waves that carried it — the worst wave wins.
|
|
246
|
+
//
|
|
247
|
+
// VOCABULARY NOTE, mirrored from direct/waveGrade.ts: `DELIVERED_WITH_DRIFT` is a member of the
|
|
248
|
+
// DELIVERED set — the batch report counts those waves as delivered with zero needing review. Drift
|
|
249
|
+
// is a fact to explain, not a failure. So drift never demotes a requirement out of "delivered";
|
|
250
|
+
// it only splits delivered into reconciled (the review-results chapter classified the drift, or
|
|
251
|
+
// the batch report closed with the wave counted delivered) vs pending (delivery done, review not).
|
|
252
|
+
const DELIVERED_VERDICTS = new Set(["DIFF_VERIFIED", "DELIVERED_WITH_DRIFT"]);
|
|
253
|
+
|
|
254
|
+
function projectResults(requirements, waves, driftReviewed, writeback) {
|
|
255
|
+
const wavesByReq = new Map();
|
|
256
|
+
for (const w of waves) {
|
|
257
|
+
for (const id of w.requirementIds) {
|
|
258
|
+
if (!wavesByReq.has(id)) wavesByReq.set(id, []);
|
|
259
|
+
wavesByReq.get(id).push(w);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
// Which wave carried each component, for the per-requirement action breakdown. A component may
|
|
263
|
+
// appear in several waves under the revision model — the LAST done wave listed is shown.
|
|
264
|
+
const waveByComponent = new Map();
|
|
265
|
+
for (const w of waves) {
|
|
266
|
+
for (const key of w.components) waveByComponent.set(key, w);
|
|
267
|
+
}
|
|
268
|
+
return requirements.map((r) => {
|
|
269
|
+
const inWaves = wavesByReq.get(r.extId) || [];
|
|
270
|
+
const doneWaves = inWaves.filter((w) => w.status === "done");
|
|
271
|
+
let outcome;
|
|
272
|
+
if (!inWaves.length) outcome = r.status === "blocked" ? "blocked" : "not-delivered";
|
|
273
|
+
else if (doneWaves.length < inWaves.length) outcome = "delivering";
|
|
274
|
+
else if (doneWaves.some((w) => !DELIVERED_VERDICTS.has(w.verdict || ""))) outcome = "problem";
|
|
275
|
+
else if (doneWaves.some((w) => w.verdict === "DELIVERED_WITH_DRIFT"))
|
|
276
|
+
outcome = driftReviewed ? "drift-reconciled" : "drift-pending";
|
|
277
|
+
else outcome = "delivered";
|
|
278
|
+
// Per-component actions: the plan says what was intended; the wave map says which wave
|
|
279
|
+
// carried it and how that wave ended.
|
|
280
|
+
const actions = (r.detail ? r.detail.components : []).map((c) => {
|
|
281
|
+
const key = `${c.typeName}:${c.fullName}`;
|
|
282
|
+
const w = waveByComponent.get(key);
|
|
283
|
+
let status;
|
|
284
|
+
if (!w) status = c.deployable ? "not-deployed" : "manual";
|
|
285
|
+
else if (w.status !== "done") status = "deploying";
|
|
286
|
+
else status = DELIVERED_VERDICTS.has(w.verdict || "") ? "verified" : "failed";
|
|
287
|
+
return { component: key, intent: c.intent || null, wave: w ? w.name : null, status };
|
|
288
|
+
});
|
|
289
|
+
// Null (not a default) when the manifest is absent or does not name this requirement — the UI
|
|
290
|
+
// reads that as "no per-row truth here" and keeps using the ledger-derived phase.
|
|
291
|
+
const wb = (writeback && writeback[r.extId]) || null;
|
|
292
|
+
return {
|
|
293
|
+
extId: r.extId,
|
|
294
|
+
title: r.title,
|
|
295
|
+
outcome,
|
|
296
|
+
waves: inWaves.map((w) => w.name),
|
|
297
|
+
components: r.detail ? r.detail.components.length : 0,
|
|
298
|
+
assumptions: r.detail ? r.detail.assumptions.length : 0,
|
|
299
|
+
actions,
|
|
300
|
+
writeback: wb
|
|
301
|
+
};
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// Per-requirement writeback state, keyed by extId, from `writeback/manifest.json` — written by
|
|
306
|
+
// campfire_write_config_items once before its concurrent upsert loop and once after the read-back.
|
|
307
|
+
// Absent for older sessions and for every run before the writeback starts, in which case the UI
|
|
308
|
+
// falls back to the one-phase-for-all-rows derivation off the chapter ledger.
|
|
309
|
+
function projectWriteback(dir) {
|
|
310
|
+
const manifest = readJson(join(dir, "writeback", "manifest.json"));
|
|
311
|
+
const rows = Array.isArray(manifest?.requirements) ? manifest.requirements : [];
|
|
312
|
+
const out = {};
|
|
313
|
+
for (const row of rows) {
|
|
314
|
+
if (!row || typeof row.extId !== "string") continue;
|
|
315
|
+
out[row.extId] = {
|
|
316
|
+
status: typeof row.status === "string" ? row.status : "pending",
|
|
317
|
+
ciCount: Number.isFinite(row.ciCount) ? row.ciCount : 0,
|
|
318
|
+
writtenAt: row.writtenAt ?? null
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
return Object.keys(out).length ? out : null;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
function projectState(id) {
|
|
325
|
+
const dir = sessionDir(id);
|
|
326
|
+
if (!dir) return null;
|
|
327
|
+
const session = readJson(join(dir, "session.json")) || {};
|
|
328
|
+
const { chapters, head, hasLedger } = projectPhases(dir);
|
|
329
|
+
const requirements = projectRequirements(dir, head);
|
|
330
|
+
const waves = projectWaves(dir);
|
|
331
|
+
const questions = projectQuestions(dir);
|
|
332
|
+
const report = readJson(join(dir, "batch-report.json"));
|
|
333
|
+
return {
|
|
334
|
+
sessionId: id,
|
|
335
|
+
generatedAt: new Date().toISOString(),
|
|
336
|
+
org: {
|
|
337
|
+
targetAlias: session.targetOrg?.alias ?? null,
|
|
338
|
+
targetOrgId: session.targetOrg?.orgId ?? null,
|
|
339
|
+
operator: session.actors?.targetUsername ?? null
|
|
340
|
+
},
|
|
341
|
+
createdAt: session.createdAt ?? null,
|
|
342
|
+
hasLedger,
|
|
343
|
+
head,
|
|
344
|
+
chapters,
|
|
345
|
+
requirements,
|
|
346
|
+
counts: {
|
|
347
|
+
total: requirements.length,
|
|
348
|
+
analyzed: requirements.filter((r) => r.status === "analyzed").length,
|
|
349
|
+
blocked: requirements.filter((r) => r.status === "blocked").length,
|
|
350
|
+
analyzing: requirements.filter((r) => r.status === "analyzing").length
|
|
351
|
+
},
|
|
352
|
+
questions,
|
|
353
|
+
waves,
|
|
354
|
+
// Drift counts as reconciled once the run has actually reviewed it: the review-results
|
|
355
|
+
// chapter completed (its ledger note is where drift gets classified), or the batch report
|
|
356
|
+
// exists (generated at close, after review, with drift waves counted delivered).
|
|
357
|
+
results: projectResults(
|
|
358
|
+
requirements,
|
|
359
|
+
waves,
|
|
360
|
+
chapters.some((c) => c.name === "review-results" && c.status === "done") || Boolean(report),
|
|
361
|
+
projectWriteback(dir)
|
|
362
|
+
),
|
|
363
|
+
report: report
|
|
364
|
+
? {
|
|
365
|
+
result: report.result,
|
|
366
|
+
deliveredWaves: report.deliveredWaves,
|
|
367
|
+
problemWaves: report.problemWaves,
|
|
368
|
+
findings: report.findings || [],
|
|
369
|
+
generatedAt: report.generatedAt
|
|
370
|
+
}
|
|
371
|
+
: null
|
|
372
|
+
};
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
function listSessions() {
|
|
376
|
+
return listDirs(SESSIONS_ROOT)
|
|
377
|
+
.filter((n) => SESSION_ID_RE.test(n))
|
|
378
|
+
.map((n) => {
|
|
379
|
+
const dir = join(SESSIONS_ROOT, n);
|
|
380
|
+
const session = readJson(join(dir, "session.json")) || {};
|
|
381
|
+
let mtime = 0;
|
|
382
|
+
try {
|
|
383
|
+
mtime = statSync(dir).mtimeMs;
|
|
384
|
+
} catch {
|
|
385
|
+
/* raced */
|
|
386
|
+
}
|
|
387
|
+
return {
|
|
388
|
+
sessionId: n,
|
|
389
|
+
createdAt: session.createdAt ?? null,
|
|
390
|
+
targetAlias: session.targetOrg?.alias ?? null,
|
|
391
|
+
requirements: (readJson(join(dir, "requirements", "index.json")) || []).length,
|
|
392
|
+
hasReport: existsSync(join(dir, "batch-report.json")),
|
|
393
|
+
mtime
|
|
394
|
+
};
|
|
395
|
+
})
|
|
396
|
+
.sort((a, b) => b.mtime - a.mtime);
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
// ---------- http --------------------------------------------------------------
|
|
400
|
+
|
|
401
|
+
const server = createServer((req, res) => {
|
|
402
|
+
const url = new URL(req.url, `http://localhost:${PORT}`);
|
|
403
|
+
const send = (code, body, type = "application/json") => {
|
|
404
|
+
res.writeHead(code, { "content-type": `${type}; charset=utf-8`, "cache-control": "no-store" });
|
|
405
|
+
res.end(type === "application/json" ? JSON.stringify(body) : body);
|
|
406
|
+
};
|
|
407
|
+
|
|
408
|
+
if (url.pathname === "/" || url.pathname === "/index.html") {
|
|
409
|
+
// Read per request: prototype-stage hot reload beats the microseconds saved by caching.
|
|
410
|
+
return send(200, readFileSync(join(HERE, "index.html"), "utf8"), "text/html");
|
|
411
|
+
}
|
|
412
|
+
if (url.pathname === "/api/sessions") {
|
|
413
|
+
return send(200, { sessions: listSessions() });
|
|
414
|
+
}
|
|
415
|
+
if (url.pathname === "/api/state") {
|
|
416
|
+
const id = url.searchParams.get("session") || newestSessionId();
|
|
417
|
+
if (!id) return send(404, { error: "no sessions found", root: SESSIONS_ROOT });
|
|
418
|
+
const state = projectState(id);
|
|
419
|
+
if (!state) return send(404, { error: `session not found: ${id}` });
|
|
420
|
+
return send(200, state);
|
|
421
|
+
}
|
|
422
|
+
if (url.pathname === "/healthz") {
|
|
423
|
+
return send(200, { ok: true, root: SESSIONS_ROOT });
|
|
424
|
+
}
|
|
425
|
+
return send(404, { error: "not found" });
|
|
426
|
+
});
|
|
427
|
+
|
|
428
|
+
server.listen(PORT, () => {
|
|
429
|
+
console.log(`IN2 session viewer — http://localhost:${PORT}`);
|
|
430
|
+
console.log(`sessions root: ${SESSIONS_ROOT}`);
|
|
431
|
+
const newest = newestSessionId();
|
|
432
|
+
if (newest) console.log(`default session: ${newest}`);
|
|
433
|
+
});
|