guardian-framework 0.1.129 → 0.1.131

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/dist/cli.js CHANGED
@@ -3202,7 +3202,7 @@ __export(exports_package, {
3202
3202
  bin: () => bin,
3203
3203
  author: () => author
3204
3204
  });
3205
- var name = "guardian-framework", version = "0.1.129", description = "Token-optimized agentic framework scaffolder with pi-first architecture", type = "module", main = "dist/exports.js", exports, types = "dist/exports.d.ts", bin, files, engines, scripts, publishConfig, pi, repository, homepage = "https://github.com/arman-jalili/guardian-framework#readme", bugs, dependencies, devDependencies, keywords, author = "Arman Wolkensteiner-Jalili", license = "MIT", package_default;
3205
+ var name = "guardian-framework", version = "0.1.131", description = "Token-optimized agentic framework scaffolder with pi-first architecture", type = "module", main = "dist/exports.js", exports, types = "dist/exports.d.ts", bin, files, engines, scripts, publishConfig, pi, repository, homepage = "https://github.com/arman-jalili/guardian-framework#readme", bugs, dependencies, devDependencies, keywords, author = "Arman Wolkensteiner-Jalili", license = "MIT", package_default;
3206
3206
  var init_package = __esm(() => {
3207
3207
  exports = {
3208
3208
  ".": {
package/dist/exports.js CHANGED
@@ -1987,7 +1987,7 @@ __export(exports_package, {
1987
1987
  bin: () => bin,
1988
1988
  author: () => author
1989
1989
  });
1990
- var name = "guardian-framework", version = "0.1.129", description = "Token-optimized agentic framework scaffolder with pi-first architecture", type = "module", main = "dist/exports.js", exports, types = "dist/exports.d.ts", bin, files, engines, scripts, publishConfig, pi, repository, homepage = "https://github.com/arman-jalili/guardian-framework#readme", bugs, dependencies, devDependencies, keywords, author = "Arman Wolkensteiner-Jalili", license = "MIT", package_default;
1990
+ var name = "guardian-framework", version = "0.1.131", description = "Token-optimized agentic framework scaffolder with pi-first architecture", type = "module", main = "dist/exports.js", exports, types = "dist/exports.d.ts", bin, files, engines, scripts, publishConfig, pi, repository, homepage = "https://github.com/arman-jalili/guardian-framework#readme", bugs, dependencies, devDependencies, keywords, author = "Arman Wolkensteiner-Jalili", license = "MIT", package_default;
1991
1991
  var init_package = __esm(() => {
1992
1992
  exports = {
1993
1993
  ".": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "guardian-framework",
3
- "version": "0.1.129",
3
+ "version": "0.1.131",
4
4
  "description": "Token-optimized agentic framework scaffolder with pi-first architecture",
5
5
  "type": "module",
6
6
  "main": "dist/exports.js",
@@ -73,6 +73,15 @@ function readLanguage(cwd: string): string {
73
73
  return "typescript";
74
74
  }
75
75
 
76
+ // Prefer the PROJECT-LOCAL guardian-framework (node_modules/.bin) over any bare
77
+ // global install. A stale global on PATH (e.g. /opt/homebrew/bin) silently ran an
78
+ // old build without the implemented-module preservation guard and clobbered docs.
79
+ function guardianBin(ctx: ExtensionContext): string {
80
+ const local = path.join(ctx.cwd, "node_modules", ".bin", "guardian-framework");
81
+ if (fs.existsSync(local)) return JSON.stringify(local);
82
+ return "npx --no-install guardian-framework";
83
+ }
84
+
76
85
  function shellExec(
77
86
  ctx: ExtensionContext,
78
87
  command: string,
@@ -240,6 +249,192 @@ function mergeRawJson(existing: Record<string, unknown>, incoming: Record<string
240
249
  }
241
250
 
242
251
  // True when a module has real implementation code (not just a planned doc).
252
+ // ── Self-driving exploration driver ─────────────────────────────────────────
253
+ // After every successful domain_save_result merge the driver inspects the saved
254
+ // session and dispatches the NEXT phase as its own small agent turn. The model is
255
+ // never asked to carry the whole protocol, and a truncation anywhere only loses
256
+ // the current small batch — /domain --resume <session> re-dispatches exactly the
257
+ // next unfinished phase from persisted state (nothing is ever re-derived).
258
+
259
+ type PhaseKind = "strategic" | "requirements" | "usecases" | "context" | "done";
260
+
261
+ function nextPhaseDecision(raw: Record<string, unknown>): { kind: PhaseKind; contextName?: string } {
262
+ const bcs = Array.isArray(raw.boundedContexts) ? (raw.boundedContexts as Record<string, unknown>[]) : [];
263
+ if (bcs.length === 0) return { kind: "strategic" };
264
+ const prog = (raw.protocolProgress as Record<string, unknown>) ?? {};
265
+ if (prog.requirementsComplete !== true) return { kind: "requirements" };
266
+ if (prog.usecasesComplete !== true) return { kind: "usecases" };
267
+ // Per-context tactical passes for every non-supporting/generic context that is
268
+ // not yet enriched (all strategic-listed entities present or any entity saved).
269
+ const supporting = new Set(bcs
270
+ .filter((bc) => /supporting|generic/i.test(String(bc.subdomainType ?? "")))
271
+ .map((bc) => String(bc.name ?? "")));
272
+ const saved = Array.isArray(raw.entities) ? (raw.entities as Record<string, unknown>[]) : [];
273
+ const byContext = new Map<string, Set<string>>();
274
+ for (const e of saved) {
275
+ const c = String(e.context ?? "");
276
+ const n = String(e.name ?? "");
277
+ if (!c || !n) continue;
278
+ if (!byContext.has(c)) byContext.set(c, new Set());
279
+ byContext.get(c)!.add(n);
280
+ }
281
+ const enriched = (name: string): boolean => {
282
+ const bc = bcs.find((x) => String(x.name ?? "") === name);
283
+ const need = (Array.isArray(bc?.entities) ? (bc!.entities as unknown[]) : []).map(String).filter(Boolean);
284
+ const have = byContext.get(name);
285
+ if (need.length > 0) return need.every((n) => have?.has(n));
286
+ return (have?.size ?? 0) > 0 || (Array.isArray(bc?.invariants) && (bc!.invariants as unknown[]).length > 0);
287
+ };
288
+ for (const bc of bcs) {
289
+ const name = String(bc.name ?? "");
290
+ if (!name || supporting.has(name) || enriched(name)) continue;
291
+ const remaining = (Array.isArray(bc.entities) ? (bc.entities as unknown[]) : [])
292
+ .map(String).filter((n) => !byContext.get(name)?.has(n));
293
+ return { kind: "context", contextName: name };
294
+ }
295
+ return { kind: "done" };
296
+ }
297
+
298
+ function maxSeqId(items: unknown[] | undefined, prefix: string): number {
299
+ let max = 0;
300
+ for (const it of (items ?? []) as Record<string, unknown>[]) {
301
+ const m = String(it.id ?? "").match(new RegExp("^" + prefix + "-(\\d+)$"));
302
+ if (m) max = Math.max(max, Number(m[1]));
303
+ }
304
+ return max;
305
+ }
306
+
307
+ const DRIVER_RULES = [
308
+ "- Reply with ONLY the small JSON for the requested step — no prose, no fences, no reasoning.",
309
+ "- Emit AT MOST 5 items per message (fewer if any item is long). Keep the JSON well under",
310
+ " ~2,500 tokens so the domain_save_result call that follows always fits in the same message.",
311
+ "- Call domain_save_result with { sessionId, responseJson: <that exact JSON> } and STOP —",
312
+ " nothing after the tool call. The driver sends your next step after it saves.",
313
+ ].join("\n");
314
+
315
+ function buildStrategicMessage(sessionId: string, businessContext: string): string {
316
+ return [
317
+ "Analyze this business domain with Domain-Driven Design. Work in SMALL incremental steps —",
318
+ "one phase per message; the driver will send each next phase after you save. This first",
319
+ "message is the STRATEGIC PASS only.",
320
+ "",
321
+ "Session: " + sessionId,
322
+ "Business Context: " + businessContext,
323
+ "",
324
+ "Reply with ONLY this JSON:",
325
+ '```json',
326
+ '{',
327
+ ' "businessContext": "one-line summary of the domain",',
328
+ ' "actors": [{ "name": "...", "description": "...", "interactions": "..." }],',
329
+ ' "assumptions": [{ "assumption": "...", "impactIfWrong": "...", "mitigation": "..." }],',
330
+ ' "boundedContexts": [',
331
+ ' { "name": "...", "description": "...", "entities": ["..."],',
332
+ ' "subdomainType": "core|supporting|generic", "subdomainRationale": "one line" }',
333
+ ' ],',
334
+ ' "contextMap": [{ "from": "A", "to": "B", "relationship": "partnership|shared-kernel|conformist|anticorruption-layer|open-host", "note": "..." }],',
335
+ ' "openQuestions": "..."',
336
+ '}',
337
+ '```',
338
+ "",
339
+ DRIVER_RULES,
340
+ ].join("\n");
341
+ }
342
+
343
+ function buildRequirementsMessage(raw: Record<string, unknown>, sessionId: string): string {
344
+ const fr = Array.isArray(raw.functionalRequirements) ? (raw.functionalRequirements as Record<string, unknown>[]) : [];
345
+ const nfr = Array.isArray(raw.nonFunctionalRequirements) ? (raw.nonFunctionalRequirements as Record<string, unknown>[]) : [];
346
+ const nextFr = maxSeqId(fr, "FR") + 1;
347
+ const nextNfr = maxSeqId(nfr, "NFR") + 1;
348
+ return [
349
+ "Session " + sessionId + " — REQUIREMENTS PASS (batch). Saved so far: " +
350
+ fr.length + " functional, " + nfr.length + " non-functional.",
351
+ "Produce the NEXT contiguous batch: up to 5 functional requirements starting at FR-" +
352
+ String(nextFr).padStart(3, "0") + (fr.length === 0 ? "" : " (continue from the last saved id)") +
353
+ " and/or up to 5 non-functional requirements starting at NFR-" + String(nextNfr).padStart(3, "0") + ".",
354
+ "Only include the type you still have left to deliver.",
355
+ "",
356
+ "Reply with ONLY this JSON:",
357
+ '```json',
358
+ '{',
359
+ ' "functionalRequirements": [',
360
+ ' { "id": "FR-001", "requirement": "The system shall...", "priority": "critical|high|medium|low", "boundedContext": "ContextName" }',
361
+ ' ],',
362
+ ' "nonFunctionalRequirements": [',
363
+ ' { "id": "NFR-001", "requirement": "The system shall...", "category": "performance|security|scalability|availability|maintainability", "target": "measurable target" }',
364
+ ' ]',
365
+ '}',
366
+ '```',
367
+ "On your FINAL requirements batch add \"requirementsComplete\": true to the JSON",
368
+ "(omit both arrays if a type is done and only send the flag).",
369
+ "",
370
+ DRIVER_RULES,
371
+ ].join("\n");
372
+ }
373
+
374
+ function buildUsecasesMessage(sessionId: string): string {
375
+ return [
376
+ "Session " + sessionId + " — USE-CASES PASS (batch). Produce up to 4 application services for the",
377
+ "core contexts (name the owning context exactly as in the strategic pass).",
378
+ "",
379
+ "Reply with ONLY this JSON:",
380
+ '```json',
381
+ '{',
382
+ ' "applicationServices": [',
383
+ ' { "name": "ApproveRequest", "context": "ApprovalWorkflow", "useCase": "what this application service orchestrates",',
384
+ ' "commands": ["verify intent", "consume"], "aggregates": ["ApprovalTicket"], "mapsToFr": "FR-00x" }',
385
+ ' ]',
386
+ '}',
387
+ '```',
388
+ "On your FINAL use-cases batch add \"usecasesComplete\": true to the JSON.",
389
+ "",
390
+ DRIVER_RULES,
391
+ ].join("\n");
392
+ }
393
+
394
+ function buildContextMessage(raw: Record<string, unknown>, sessionId: string, contextName: string): string {
395
+ const bc = (Array.isArray(raw.boundedContexts) ? (raw.boundedContexts as Record<string, unknown>[]) : [])
396
+ .find((x) => String(x.name ?? "") === contextName);
397
+ const need = (Array.isArray(bc?.entities) ? (bc!.entities as unknown[]) : []).map(String).filter(Boolean);
398
+ const done = Array.isArray(raw.entities) ? (raw.entities as Record<string, unknown>[])
399
+ .filter((e) => String(e.context ?? "") === contextName).map((e) => String(e.name ?? "")) : [];
400
+ const remaining = need.filter((n) => !done.includes(n));
401
+ return [
402
+ "Session " + sessionId + " — PER-CONTEXT PASS for \"" + contextName + "\"" +
403
+ (remaining.length > 0 ? " (still needed from strategic list: " + remaining.join(", ") + ")" : "") + ".",
404
+ "Enrich this ONE context: entities with entity-vs-VO justification, domain events with triage,",
405
+ "invariants, and this context's ubiquitous language. Up to 6 entities per message — if the",
406
+ "context has more, the driver will ask you to continue this same context.",
407
+ "",
408
+ "Reply with ONLY this JSON:",
409
+ '```json',
410
+ '{',
411
+ ' "boundedContexts": [{ "name": "' + contextName + '", "description": "...", "entities": ["Entity1"],',
412
+ ' "invariants": [{ "rule": "...", "illegalStatePrevented": "..." }] }],',
413
+ ' "entities": [',
414
+ ' { "name": "Entity1", "context": "' + contextName + '", "type": "entity|value-object|aggregate-root",',
415
+ ' "description": "...",',
416
+ ' "justification": { "identityPersists": true, "immutable": false, "equalByState": false } }',
417
+ ' ],',
418
+ ' "domainEvents": [',
419
+ ' { "name": "Event", "context": "' + contextName + '", "description": "...", "triggeredBy": "...",',
420
+ ' "triage": { "domainExpertCares": true, "crossesAggregateBoundary": false, "needsAudit": false } }',
421
+ ' ],',
422
+ ' "ubiquitousLanguage": [{ "term": "...", "definition": "...", "boundedContext": "' + contextName + '", "aliases": [], "examples": "" }]',
423
+ '}',
424
+ '```',
425
+ "",
426
+ DRIVER_RULES,
427
+ ].join("\n");
428
+ }
429
+
430
+ function buildPhaseMessage(kind: PhaseKind, raw: Record<string, unknown>, sessionId: string, contextName?: string): string {
431
+ if (kind === "strategic") return buildStrategicMessage(sessionId, String(raw.businessContext ?? ""));
432
+ if (kind === "requirements") return buildRequirementsMessage(raw, sessionId);
433
+ if (kind === "usecases") return buildUsecasesMessage(sessionId);
434
+ if (kind === "context") return buildContextMessage(raw, sessionId, contextName ?? "");
435
+ return "";
436
+ }
437
+
243
438
  function moduleHasCode(cwd: string, moduleName: string): boolean {
244
439
  const sanitized = (moduleName || "").toLowerCase().replace(/[^a-z0-9]/g, "");
245
440
  if (!sanitized) return false;
@@ -461,6 +656,14 @@ export default function (pi: ExtensionAPI) {
461
656
  return { content: [{ type: "text" as const, text: "ERROR: Failed to parse JSON: " + String(e) }] };
462
657
  }
463
658
 
659
+ // Phase-completion flags ride along on the model's JSON but are not part of
660
+ // the persisted schema — pull them off before merging so they never pollute
661
+ // the raw session, then fold them into protocolProgress below.
662
+ const flagsRc = parsedJson.requirementsComplete === true;
663
+ const flagsUc = parsedJson.usecasesComplete === true;
664
+ delete parsedJson.requirementsComplete;
665
+ delete parsedJson.usecasesComplete;
666
+
464
667
  // Incremental: merge into any previously saved structured session
465
668
  // (<session>.raw.json) so large domains can be saved in parts.
466
669
  const rawPath = path.join(ctx.cwd, ".pi", "domain", "exploration", sessionId + ".raw.json");
@@ -476,6 +679,19 @@ export default function (pi: ExtensionAPI) {
476
679
  parsedJson.status = String(parsedJson.status ?? "draft");
477
680
  parsedJson.createdAt = parsedJson.createdAt ?? new Date().toISOString();
478
681
 
682
+ // Self-driving protocol progress. The driver asks the model ONE phase at a
683
+ // time; these markers say which phases are finished. Stored separately so
684
+ // the model schema stays clean and id-keyed merges never disturb them.
685
+ const prevRawForProg = (() => {
686
+ try { return JSON.parse(fs.readFileSync(rawPath, "utf-8")) as Record<string, unknown>; }
687
+ catch { return {}; }
688
+ })();
689
+ const prevProg = (prevRawForProg.protocolProgress as Record<string, unknown>) ?? {};
690
+ parsedJson.protocolProgress = {
691
+ requirementsComplete: prevProg.requirementsComplete === true || flagsRc,
692
+ usecasesComplete: prevProg.usecasesComplete === true || flagsUc,
693
+ };
694
+
479
695
  // Derive aggregate roots from the entities table — never let the
480
696
  // rendered "## Aggregate Roots" contradict entities marked
481
697
  // aggregate-root (the protocol passes don't carry this field).
@@ -610,7 +826,27 @@ export default function (pi: ExtensionAPI) {
610
826
  }
611
827
 
612
828
  onUpdate?.({ content: [{ type: "text", text: "Domain exploration saved for session: " + sessionId }] });
613
- return { content: [{ type: "text" as const, text: "Domain exploration saved for " + sessionId + ". Next: /domain --architect-scaffold " + sessionId + " or review .pi/domain/exploration.md" }] };
829
+
830
+ // Self-driving: after every successful save, dispatch the next phase as a
831
+ // fresh small agent turn (or tell the model exploration is complete). If a
832
+ // phase needs more batches (requirements >5, context >6 entities) the same
833
+ // decision re-issues that phase with progress hints — never a reset.
834
+ const decision = nextPhaseDecision(parsedJson);
835
+ const savedNote = "Domain exploration saved for " + sessionId + ".";
836
+ if (decision.kind === "done") {
837
+ return { content: [{ type: "text" as const, text: savedNote + " Exploration content complete — run /domain --validate " + sessionId }] };
838
+ }
839
+ const label = decision.kind === "context" ? "context " + (decision.contextName ?? "") : decision.kind;
840
+ const nextMsg = buildPhaseMessage(decision.kind, parsedJson, sessionId, decision.contextName);
841
+ try {
842
+ pi.sendMessage(
843
+ { content: nextMsg, display: true },
844
+ { deliverAs: "followUp", triggerTurn: true },
845
+ );
846
+ return { content: [{ type: "text" as const, text: savedNote + " Next phase dispatched: " + label }] };
847
+ } catch {
848
+ return { content: [{ type: "text" as const, text: savedNote + "\n\nNEXT PHASE (" + label + "):\n" + nextMsg }] };
849
+ }
614
850
  },
615
851
  });
616
852
 
@@ -919,86 +1155,7 @@ export default function (pi: ExtensionAPI) {
919
1155
  // Inject the DDD analysis prompt as a follow-up message to the agent.
920
1156
  // Using sendMessage with triggerTurn=true causes the agent to process
921
1157
  // this as a new conversation turn, not as a command response.
922
- const analysisPrompt = [
923
- "Analyze this business domain with Domain-Driven Design. You MUST work in SMALL",
924
- "incremental steps — never produce the whole model in one message, or you will be",
925
- "truncated mid-JSON and lose work.",
926
- "",
927
- "Session: " + sessionId,
928
- "Business Context: " + sanitized,
929
- "",
930
- "STEP PROTOCOL (strict — one step per message, each message <= ~1,500 tokens of JSON):",
931
- "",
932
- "1. STRATEGIC PASS (this message): reply with ONLY this JSON, nothing else —",
933
- '```json',
934
- '{',
935
- ' "businessContext": "one-line summary of the domain",',
936
- ' "actors": [{ "name": "...", "description": "...", "interactions": "..." }],',
937
- ' "assumptions": [{ "assumption": "...", "impactIfWrong": "...", "mitigation": "..." }],',
938
- ' "boundedContexts": [',
939
- ' { "name": "...", "description": "...", "entities": ["..."],',
940
- ' "subdomainType": "core|supporting|generic", "subdomainRationale": "one line" }',
941
- ' ],',
942
- ' "contextMap": [{ "from": "A", "to": "B", "relationship": "partnership|shared-kernel|conformist|anticorruption-layer|open-host", "note": "..." }],',
943
- ' "openQuestions": "..."',
944
- '}',
945
- '```',
946
- "then call domain_save_result with { sessionId, responseJson: <that JSON> }. It merges;",
947
- "the file is saved for you. Do NOT also edit exploration.md by hand.",
948
- "",
949
- "1b. REQUIREMENTS PASS (next message): reply with ONLY this JSON, then call domain_save_result",
950
- "again (merge is by id — existing entries are untouched):",
951
- '```json',
952
- '{',
953
- ' "functionalRequirements": [',
954
- ' { "id": "FR-001", "requirement": "The system shall...", "priority": "critical|high|medium|low", "boundedContext": "ContextName" }',
955
- ' ],',
956
- ' "nonFunctionalRequirements": [',
957
- ' { "id": "NFR-001", "requirement": "The system shall...", "category": "performance|security|scalability|availability|maintainability", "target": "measurable target" }',
958
- ' ]',
959
- '}',
960
- '```',
961
- "",
962
- "1c. USE-CASES PASS (next message): reply with ONLY this JSON for ALL core contexts, then call",
963
- "domain_save_result (merge is by name+context):",
964
- '```json',
965
- '{',
966
- ' "applicationServices": [',
967
- ' { "name": "ApproveRequest", "context": "ApprovalWorkflow", "useCase": "what this application service orchestrates",',
968
- ' "commands": ["verify intent", "consume"], "aggregates": ["ApprovalTicket"], "mapsToFr": "FR-00x" }',
969
- ' ]',
970
- '}',
971
- '```',
972
- "",
973
- "2. PER-CONTEXT PASS (repeat for EACH core/tactical bounded context, one context per message):",
974
- "reply with ONLY this JSON for one context, then call domain_save_result again:",
975
- '```json',
976
- '{',
977
- ' "boundedContexts": [{ "name": "TheContext", "description": "...", "entities": ["Entity1"],',
978
- ' "invariants": [{ "rule": "...", "illegalStatePrevented": "..." }] }],',
979
- ' "entities": [',
980
- ' { "name": "Entity1", "context": "TheContext", "type": "entity|value-object|aggregate-root",',
981
- ' "description": "...",',
982
- ' "justification": { "identityPersists": true, "immutable": false, "equalByState": false } }',
983
- ' ],',
984
- ' "domainEvents": [',
985
- ' { "name": "Event", "context": "TheContext", "description": "...", "triggeredBy": "...",',
986
- ' "triage": { "domainExpertCares": true, "crossesAggregateBoundary": false, "needsAudit": false } }',
987
- ' ],',
988
- ' "ubiquitousLanguage": [{ "term": "...", "definition": "...", "boundedContext": "TheContext", "aliases": [], "examples": "" }]',
989
- '}',
990
- '```',
991
- "Supporting/generic contexts need no detail pass — the strategic entry is enough.",
992
- "",
993
- "RULES:",
994
- "- If a message of yours is cut off mid-JSON, continue from exactly where you stopped in the",
995
- " NEXT message (never restart the whole model).",
996
- "- Never emit the full model in one response and never exceed ~1,500 tokens of JSON per message.",
997
- "- HARD RULE: never narrate or reason in chat. No prose analysis, no relationship",
998
- " deliberation, no summaries — those burn your output budget and truncate the JSON.",
999
- " Emit ONLY the small JSON step for the current phase and stop.",
1000
- "- After the last context, run /domain --validate " + sessionId + " and report the checks.",
1001
- ].join("\n");
1158
+ const analysisPrompt = buildStrategicMessage(sessionId, sanitized);
1002
1159
 
1003
1160
 
1004
1161
  try {
@@ -1014,6 +1171,43 @@ export default function (pi: ExtensionAPI) {
1014
1171
  return "Domain analysis dispatched to agent. Session: " + sessionId;
1015
1172
  }
1016
1173
 
1174
+ // /domain --resume <session-id> — re-dispatch the next unfinished phase from
1175
+ // persisted state. Truncation anywhere in the protocol is recoverable with
1176
+ // this: it never asks the model to reproduce saved content.
1177
+ if (trimmed.startsWith("--resume")) {
1178
+ const sessionId = trimmed.slice("--resume".length).trim().split(/\s+/)[0] || "";
1179
+ if (!sessionId) {
1180
+ ctx.ui.notify("Usage: /domain --resume <session-id>", "error");
1181
+ return "(domain command handled)";
1182
+ }
1183
+ const explorationDir = path.join(ctx.cwd, ".pi", "domain", "exploration");
1184
+ const rawPath = path.join(explorationDir, sessionId + ".raw.json");
1185
+ if (!fs.existsSync(rawPath)) {
1186
+ ctx.ui.notify("Session not found: " + sessionId + ". Run /domain --explore first.", "error");
1187
+ return "(domain command handled)";
1188
+ }
1189
+ let raw: Record<string, unknown> = {};
1190
+ try { raw = JSON.parse(fs.readFileSync(rawPath, "utf-8")) as Record<string, unknown>; }
1191
+ catch { ctx.ui.notify("Session raw state unreadable for " + sessionId, "error"); return "(domain command handled)"; }
1192
+ const decision = nextPhaseDecision(raw);
1193
+ if (decision.kind === "done") {
1194
+ ctx.ui.notify("Exploration content is complete — run /domain --validate " + sessionId, "info");
1195
+ return "(domain command handled)";
1196
+ }
1197
+ const label = decision.kind === "context" ? "context " + decision.contextName : decision.kind;
1198
+ ctx.ui.notify("Resumed " + sessionId + " at phase: " + label, "success");
1199
+ const msg = buildPhaseMessage(decision.kind, raw, sessionId, decision.contextName);
1200
+ try {
1201
+ pi.sendMessage(
1202
+ { content: msg, display: true },
1203
+ { deliverAs: "followUp", triggerTurn: true },
1204
+ );
1205
+ return "Resumed " + sessionId + " at phase: " + label;
1206
+ } catch {
1207
+ return msg;
1208
+ }
1209
+ }
1210
+
1017
1211
  // /domain --architect-scaffold <session-id>
1018
1212
  // /domain --architect-scaffold <session-id>
1019
1213
  if (trimmed.startsWith("--architect-scaffold")) {
@@ -1097,7 +1291,7 @@ export default function (pi: ExtensionAPI) {
1097
1291
  let scaffoldError = "";
1098
1292
 
1099
1293
  try {
1100
- const cmd = "guardian-framework domain scaffold " + sessionId + (forceScaffold ? " --force" : "") + " 2>&1";
1294
+ const cmd = guardianBin(ctx) + " domain scaffold " + sessionId + (forceScaffold ? " --force" : "") + " 2>&1";
1101
1295
  const shellResult = await shellExec(ctx, cmd);
1102
1296
  if (shellResult.exitCode === 0) {
1103
1297
  const outputLines = shellResult.stdout.split("\n");
@@ -1,5 +1,5 @@
1
1
  {
2
- "timestamp": "2026-09-06T13:20:31Z",
2
+ "timestamp": "2026-09-06T15:38:53Z",
3
3
  "mode": "all",
4
4
  "stages_run": [],
5
5
  "summary": {