guardian-framework 0.1.128 → 0.1.130

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.128", 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.130", 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
  ".": {
@@ -3213,7 +3213,10 @@ var init_package = __esm(() => {
3213
3213
  bin = {
3214
3214
  "guardian-framework": "dist/cli.js"
3215
3215
  };
3216
- files = ["dist/", "templates/"];
3216
+ files = [
3217
+ "dist/",
3218
+ "templates/"
3219
+ ];
3217
3220
  engines = {
3218
3221
  bun: ">=1.0.0"
3219
3222
  };
@@ -3231,9 +3234,15 @@ var init_package = __esm(() => {
3231
3234
  access: "public"
3232
3235
  };
3233
3236
  pi = {
3234
- extensions: ["./templates/pi/extensions"],
3235
- skills: ["./templates/pi/skills"],
3236
- prompts: ["./templates/pi/prompts"]
3237
+ extensions: [
3238
+ "./templates/pi/extensions"
3239
+ ],
3240
+ skills: [
3241
+ "./templates/pi/skills"
3242
+ ],
3243
+ prompts: [
3244
+ "./templates/pi/prompts"
3245
+ ]
3237
3246
  };
3238
3247
  repository = {
3239
3248
  type: "git",
@@ -12178,6 +12187,60 @@ function answerExploration(sessionId, responseJson, options) {
12178
12187
  warnings
12179
12188
  };
12180
12189
  }
12190
+ function moduleHasImplementation(projectDir, moduleName) {
12191
+ const sanitized = (moduleName || "").toLowerCase().replace(/[^a-z0-9]/g, "");
12192
+ if (!sanitized)
12193
+ return false;
12194
+ const roots = ["src", "crates", "packages", "internal", "lib", "app", "modules"];
12195
+ const exts = ".*.(java|kt|ts|tsx|js|jsx|rs|py|go)$";
12196
+ for (const root of roots) {
12197
+ const base = path2.join(projectDir, root);
12198
+ if (!fs.existsSync(base))
12199
+ continue;
12200
+ try {
12201
+ const walk = (dir, depth) => {
12202
+ if (depth > 6)
12203
+ return false;
12204
+ let entries;
12205
+ try {
12206
+ entries = fs.readdirSync(dir, { withFileTypes: true });
12207
+ } catch {
12208
+ return false;
12209
+ }
12210
+ for (const e of entries) {
12211
+ if (e.name === "node_modules" || e.name === "target" || e.name === "dist" || e.name === ".git")
12212
+ continue;
12213
+ const full = path2.join(dir, e.name);
12214
+ if (e.isDirectory()) {
12215
+ if (e.name === sanitized && hasSourceBelow(full, depth))
12216
+ return true;
12217
+ if (walk(full, depth + 1))
12218
+ return true;
12219
+ }
12220
+ }
12221
+ return false;
12222
+ };
12223
+ const hasSourceBelow = (dir, depth) => {
12224
+ let entries;
12225
+ try {
12226
+ entries = fs.readdirSync(dir, { withFileTypes: true });
12227
+ } catch {
12228
+ return false;
12229
+ }
12230
+ for (const e of entries) {
12231
+ if (e.isFile() && new RegExp(exts).test(e.name) && !/test|spec/i.test(e.name))
12232
+ return true;
12233
+ if (e.isDirectory() && e.name !== "node_modules" && e.name !== "target" && depth < 6 && hasSourceBelow(path2.join(dir, e.name), depth + 1))
12234
+ return true;
12235
+ }
12236
+ return false;
12237
+ };
12238
+ if (walk(base, 0))
12239
+ return true;
12240
+ } catch {}
12241
+ }
12242
+ return false;
12243
+ }
12181
12244
  function readExplorationSession(sessionId, projectDir) {
12182
12245
  const root = projectDir ?? process.cwd();
12183
12246
  const filePath = path2.join(root, ".pi", "domain", "exploration", `${sessionId}.md`);
@@ -12378,11 +12441,16 @@ function scaffoldFromExploration(sessionId, options) {
12378
12441
  const invariants = bc.invariants || [];
12379
12442
  const force = options?.overwrite === true;
12380
12443
  if (!dryRun && fs.existsSync(filePath)) {
12381
- if (!force) {
12382
- warnings.push(`Module already exists, preserved (use overwrite:true to replace): ${fileName}`);
12444
+ const implemented = moduleHasImplementation(projectDir, moduleName);
12445
+ if (implemented && !force) {
12446
+ warnings.push(`Module is IMPLEMENTED, preserved (use overwrite:true to force): ${fileName}`);
12383
12447
  continue;
12384
12448
  }
12385
- warnings.push(`Module already exists, will be overwritten: ${fileName}`);
12449
+ if (implemented && force) {
12450
+ warnings.push(`Overwriting implemented module doc (--force): ${fileName}`);
12451
+ } else {
12452
+ warnings.push(`Refreshing planned module doc (no implementation yet): ${fileName}`);
12453
+ }
12386
12454
  }
12387
12455
  const aggregateRoots = contextEntities.filter((e) => e.type === "aggregate-root");
12388
12456
  const regularEntities = contextEntities.filter((e) => e.type === "entity");
@@ -13155,7 +13223,7 @@ async function runDomain(targetDir, args, options) {
13155
13223
  await runExplore(targetDir, options);
13156
13224
  break;
13157
13225
  case "scaffold":
13158
- await runScaffold(targetDir, args[1], options);
13226
+ await runScaffold(targetDir, args[1], { ...options, overwrite: args.includes("--force") });
13159
13227
  break;
13160
13228
  case "answer":
13161
13229
  await runAnswer(targetDir, args[1], args[2], options);
@@ -13262,7 +13330,8 @@ async function runScaffold(targetDir, sessionId, options) {
13262
13330
  try {
13263
13331
  const result = scaffoldFromExploration(sessionId, {
13264
13332
  projectDir: targetDir,
13265
- dryRun: options.dryRun
13333
+ dryRun: options.dryRun,
13334
+ overwrite: options.overwrite
13266
13335
  });
13267
13336
  if (options.dryRun) {
13268
13337
  outro(`Dry-run: would generate ${result.modulesGenerated} module(s)`);
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.128", 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.130", 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
  ".": {
@@ -1998,7 +1998,10 @@ var init_package = __esm(() => {
1998
1998
  bin = {
1999
1999
  "guardian-framework": "dist/cli.js"
2000
2000
  };
2001
- files = ["dist/", "templates/"];
2001
+ files = [
2002
+ "dist/",
2003
+ "templates/"
2004
+ ];
2002
2005
  engines = {
2003
2006
  bun: ">=1.0.0"
2004
2007
  };
@@ -2016,9 +2019,15 @@ var init_package = __esm(() => {
2016
2019
  access: "public"
2017
2020
  };
2018
2021
  pi = {
2019
- extensions: ["./templates/pi/extensions"],
2020
- skills: ["./templates/pi/skills"],
2021
- prompts: ["./templates/pi/prompts"]
2022
+ extensions: [
2023
+ "./templates/pi/extensions"
2024
+ ],
2025
+ skills: [
2026
+ "./templates/pi/skills"
2027
+ ],
2028
+ prompts: [
2029
+ "./templates/pi/prompts"
2030
+ ]
2022
2031
  };
2023
2032
  repository = {
2024
2033
  type: "git",
@@ -12502,6 +12511,60 @@ function answerExploration(sessionId, responseJson, options) {
12502
12511
  warnings
12503
12512
  };
12504
12513
  }
12514
+ function moduleHasImplementation(projectDir, moduleName) {
12515
+ const sanitized = (moduleName || "").toLowerCase().replace(/[^a-z0-9]/g, "");
12516
+ if (!sanitized)
12517
+ return false;
12518
+ const roots = ["src", "crates", "packages", "internal", "lib", "app", "modules"];
12519
+ const exts = ".*.(java|kt|ts|tsx|js|jsx|rs|py|go)$";
12520
+ for (const root of roots) {
12521
+ const base = path11.join(projectDir, root);
12522
+ if (!fs12.existsSync(base))
12523
+ continue;
12524
+ try {
12525
+ const walk = (dir, depth) => {
12526
+ if (depth > 6)
12527
+ return false;
12528
+ let entries;
12529
+ try {
12530
+ entries = fs12.readdirSync(dir, { withFileTypes: true });
12531
+ } catch {
12532
+ return false;
12533
+ }
12534
+ for (const e of entries) {
12535
+ if (e.name === "node_modules" || e.name === "target" || e.name === "dist" || e.name === ".git")
12536
+ continue;
12537
+ const full = path11.join(dir, e.name);
12538
+ if (e.isDirectory()) {
12539
+ if (e.name === sanitized && hasSourceBelow(full, depth))
12540
+ return true;
12541
+ if (walk(full, depth + 1))
12542
+ return true;
12543
+ }
12544
+ }
12545
+ return false;
12546
+ };
12547
+ const hasSourceBelow = (dir, depth) => {
12548
+ let entries;
12549
+ try {
12550
+ entries = fs12.readdirSync(dir, { withFileTypes: true });
12551
+ } catch {
12552
+ return false;
12553
+ }
12554
+ for (const e of entries) {
12555
+ if (e.isFile() && new RegExp(exts).test(e.name) && !/test|spec/i.test(e.name))
12556
+ return true;
12557
+ if (e.isDirectory() && e.name !== "node_modules" && e.name !== "target" && depth < 6 && hasSourceBelow(path11.join(dir, e.name), depth + 1))
12558
+ return true;
12559
+ }
12560
+ return false;
12561
+ };
12562
+ if (walk(base, 0))
12563
+ return true;
12564
+ } catch {}
12565
+ }
12566
+ return false;
12567
+ }
12505
12568
  function readExplorationSession(sessionId, projectDir) {
12506
12569
  const root = projectDir ?? process.cwd();
12507
12570
  const filePath = path11.join(root, ".pi", "domain", "exploration", `${sessionId}.md`);
@@ -12702,11 +12765,16 @@ function scaffoldFromExploration(sessionId, options) {
12702
12765
  const invariants = bc.invariants || [];
12703
12766
  const force = options?.overwrite === true;
12704
12767
  if (!dryRun && fs12.existsSync(filePath)) {
12705
- if (!force) {
12706
- warnings.push(`Module already exists, preserved (use overwrite:true to replace): ${fileName}`);
12768
+ const implemented = moduleHasImplementation(projectDir, moduleName);
12769
+ if (implemented && !force) {
12770
+ warnings.push(`Module is IMPLEMENTED, preserved (use overwrite:true to force): ${fileName}`);
12707
12771
  continue;
12708
12772
  }
12709
- warnings.push(`Module already exists, will be overwritten: ${fileName}`);
12773
+ if (implemented && force) {
12774
+ warnings.push(`Overwriting implemented module doc (--force): ${fileName}`);
12775
+ } else {
12776
+ warnings.push(`Refreshing planned module doc (no implementation yet): ${fileName}`);
12777
+ }
12710
12778
  }
12711
12779
  const aggregateRoots = contextEntities.filter((e) => e.type === "aggregate-root");
12712
12780
  const regularEntities = contextEntities.filter((e) => e.type === "entity");
@@ -13479,7 +13547,7 @@ async function runDomain(targetDir, args, options) {
13479
13547
  await runExplore(targetDir, options);
13480
13548
  break;
13481
13549
  case "scaffold":
13482
- await runScaffold(targetDir, args[1], options);
13550
+ await runScaffold(targetDir, args[1], { ...options, overwrite: args.includes("--force") });
13483
13551
  break;
13484
13552
  case "answer":
13485
13553
  await runAnswer(targetDir, args[1], args[2], options);
@@ -13586,7 +13654,8 @@ async function runScaffold(targetDir, sessionId, options) {
13586
13654
  try {
13587
13655
  const result = scaffoldFromExploration(sessionId, {
13588
13656
  projectDir: targetDir,
13589
- dryRun: options.dryRun
13657
+ dryRun: options.dryRun,
13658
+ overwrite: options.overwrite
13590
13659
  });
13591
13660
  if (options.dryRun) {
13592
13661
  outro(`Dry-run: would generate ${result.modulesGenerated} module(s)`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "guardian-framework",
3
- "version": "0.1.128",
3
+ "version": "0.1.130",
4
4
  "description": "Token-optimized agentic framework scaffolder with pi-first architecture",
5
5
  "type": "module",
6
6
  "main": "dist/exports.js",
@@ -14,7 +14,10 @@
14
14
  "bin": {
15
15
  "guardian-framework": "dist/cli.js"
16
16
  },
17
- "files": ["dist/", "templates/"],
17
+ "files": [
18
+ "dist/",
19
+ "templates/"
20
+ ],
18
21
  "engines": {
19
22
  "bun": ">=1.0.0"
20
23
  },
@@ -32,9 +35,15 @@
32
35
  "access": "public"
33
36
  },
34
37
  "pi": {
35
- "extensions": ["./templates/pi/extensions"],
36
- "skills": ["./templates/pi/skills"],
37
- "prompts": ["./templates/pi/prompts"]
38
+ "extensions": [
39
+ "./templates/pi/extensions"
40
+ ],
41
+ "skills": [
42
+ "./templates/pi/skills"
43
+ ],
44
+ "prompts": [
45
+ "./templates/pi/prompts"
46
+ ]
38
47
  },
39
48
  "repository": {
40
49
  "type": "git",
@@ -239,6 +239,231 @@ function mergeRawJson(existing: Record<string, unknown>, incoming: Record<string
239
239
  return merged;
240
240
  }
241
241
 
242
+ // True when a module has real implementation code (not just a planned doc).
243
+ // ── Self-driving exploration driver ─────────────────────────────────────────
244
+ // After every successful domain_save_result merge the driver inspects the saved
245
+ // session and dispatches the NEXT phase as its own small agent turn. The model is
246
+ // never asked to carry the whole protocol, and a truncation anywhere only loses
247
+ // the current small batch — /domain --resume <session> re-dispatches exactly the
248
+ // next unfinished phase from persisted state (nothing is ever re-derived).
249
+
250
+ type PhaseKind = "strategic" | "requirements" | "usecases" | "context" | "done";
251
+
252
+ function nextPhaseDecision(raw: Record<string, unknown>): { kind: PhaseKind; contextName?: string } {
253
+ const bcs = Array.isArray(raw.boundedContexts) ? (raw.boundedContexts as Record<string, unknown>[]) : [];
254
+ if (bcs.length === 0) return { kind: "strategic" };
255
+ const prog = (raw.protocolProgress as Record<string, unknown>) ?? {};
256
+ if (prog.requirementsComplete !== true) return { kind: "requirements" };
257
+ if (prog.usecasesComplete !== true) return { kind: "usecases" };
258
+ // Per-context tactical passes for every non-supporting/generic context that is
259
+ // not yet enriched (all strategic-listed entities present or any entity saved).
260
+ const supporting = new Set(bcs
261
+ .filter((bc) => /supporting|generic/i.test(String(bc.subdomainType ?? "")))
262
+ .map((bc) => String(bc.name ?? "")));
263
+ const saved = Array.isArray(raw.entities) ? (raw.entities as Record<string, unknown>[]) : [];
264
+ const byContext = new Map<string, Set<string>>();
265
+ for (const e of saved) {
266
+ const c = String(e.context ?? "");
267
+ const n = String(e.name ?? "");
268
+ if (!c || !n) continue;
269
+ if (!byContext.has(c)) byContext.set(c, new Set());
270
+ byContext.get(c)!.add(n);
271
+ }
272
+ const enriched = (name: string): boolean => {
273
+ const bc = bcs.find((x) => String(x.name ?? "") === name);
274
+ const need = (Array.isArray(bc?.entities) ? (bc!.entities as unknown[]) : []).map(String).filter(Boolean);
275
+ const have = byContext.get(name);
276
+ if (need.length > 0) return need.every((n) => have?.has(n));
277
+ return (have?.size ?? 0) > 0 || (Array.isArray(bc?.invariants) && (bc!.invariants as unknown[]).length > 0);
278
+ };
279
+ for (const bc of bcs) {
280
+ const name = String(bc.name ?? "");
281
+ if (!name || supporting.has(name) || enriched(name)) continue;
282
+ const remaining = (Array.isArray(bc.entities) ? (bc.entities as unknown[]) : [])
283
+ .map(String).filter((n) => !byContext.get(name)?.has(n));
284
+ return { kind: "context", contextName: name };
285
+ }
286
+ return { kind: "done" };
287
+ }
288
+
289
+ function maxSeqId(items: unknown[] | undefined, prefix: string): number {
290
+ let max = 0;
291
+ for (const it of (items ?? []) as Record<string, unknown>[]) {
292
+ const m = String(it.id ?? "").match(new RegExp("^" + prefix + "-(\\d+)$"));
293
+ if (m) max = Math.max(max, Number(m[1]));
294
+ }
295
+ return max;
296
+ }
297
+
298
+ const DRIVER_RULES = [
299
+ "- Reply with ONLY the small JSON for the requested step — no prose, no fences, no reasoning.",
300
+ "- Emit AT MOST 5 items per message (fewer if any item is long). Keep the JSON well under",
301
+ " ~2,500 tokens so the domain_save_result call that follows always fits in the same message.",
302
+ "- Call domain_save_result with { sessionId, responseJson: <that exact JSON> } and STOP —",
303
+ " nothing after the tool call. The driver sends your next step after it saves.",
304
+ ].join("\n");
305
+
306
+ function buildStrategicMessage(sessionId: string, businessContext: string): string {
307
+ return [
308
+ "Analyze this business domain with Domain-Driven Design. Work in SMALL incremental steps —",
309
+ "one phase per message; the driver will send each next phase after you save. This first",
310
+ "message is the STRATEGIC PASS only.",
311
+ "",
312
+ "Session: " + sessionId,
313
+ "Business Context: " + businessContext,
314
+ "",
315
+ "Reply with ONLY this JSON:",
316
+ '```json',
317
+ '{',
318
+ ' "businessContext": "one-line summary of the domain",',
319
+ ' "actors": [{ "name": "...", "description": "...", "interactions": "..." }],',
320
+ ' "assumptions": [{ "assumption": "...", "impactIfWrong": "...", "mitigation": "..." }],',
321
+ ' "boundedContexts": [',
322
+ ' { "name": "...", "description": "...", "entities": ["..."],',
323
+ ' "subdomainType": "core|supporting|generic", "subdomainRationale": "one line" }',
324
+ ' ],',
325
+ ' "contextMap": [{ "from": "A", "to": "B", "relationship": "partnership|shared-kernel|conformist|anticorruption-layer|open-host", "note": "..." }],',
326
+ ' "openQuestions": "..."',
327
+ '}',
328
+ '```',
329
+ "",
330
+ DRIVER_RULES,
331
+ ].join("\n");
332
+ }
333
+
334
+ function buildRequirementsMessage(raw: Record<string, unknown>, sessionId: string): string {
335
+ const fr = Array.isArray(raw.functionalRequirements) ? (raw.functionalRequirements as Record<string, unknown>[]) : [];
336
+ const nfr = Array.isArray(raw.nonFunctionalRequirements) ? (raw.nonFunctionalRequirements as Record<string, unknown>[]) : [];
337
+ const nextFr = maxSeqId(fr, "FR") + 1;
338
+ const nextNfr = maxSeqId(nfr, "NFR") + 1;
339
+ return [
340
+ "Session " + sessionId + " — REQUIREMENTS PASS (batch). Saved so far: " +
341
+ fr.length + " functional, " + nfr.length + " non-functional.",
342
+ "Produce the NEXT contiguous batch: up to 5 functional requirements starting at FR-" +
343
+ String(nextFr).padStart(3, "0") + (fr.length === 0 ? "" : " (continue from the last saved id)") +
344
+ " and/or up to 5 non-functional requirements starting at NFR-" + String(nextNfr).padStart(3, "0") + ".",
345
+ "Only include the type you still have left to deliver.",
346
+ "",
347
+ "Reply with ONLY this JSON:",
348
+ '```json',
349
+ '{',
350
+ ' "functionalRequirements": [',
351
+ ' { "id": "FR-001", "requirement": "The system shall...", "priority": "critical|high|medium|low", "boundedContext": "ContextName" }',
352
+ ' ],',
353
+ ' "nonFunctionalRequirements": [',
354
+ ' { "id": "NFR-001", "requirement": "The system shall...", "category": "performance|security|scalability|availability|maintainability", "target": "measurable target" }',
355
+ ' ]',
356
+ '}',
357
+ '```',
358
+ "On your FINAL requirements batch add \"requirementsComplete\": true to the JSON",
359
+ "(omit both arrays if a type is done and only send the flag).",
360
+ "",
361
+ DRIVER_RULES,
362
+ ].join("\n");
363
+ }
364
+
365
+ function buildUsecasesMessage(sessionId: string): string {
366
+ return [
367
+ "Session " + sessionId + " — USE-CASES PASS (batch). Produce up to 4 application services for the",
368
+ "core contexts (name the owning context exactly as in the strategic pass).",
369
+ "",
370
+ "Reply with ONLY this JSON:",
371
+ '```json',
372
+ '{',
373
+ ' "applicationServices": [',
374
+ ' { "name": "ApproveRequest", "context": "ApprovalWorkflow", "useCase": "what this application service orchestrates",',
375
+ ' "commands": ["verify intent", "consume"], "aggregates": ["ApprovalTicket"], "mapsToFr": "FR-00x" }',
376
+ ' ]',
377
+ '}',
378
+ '```',
379
+ "On your FINAL use-cases batch add \"usecasesComplete\": true to the JSON.",
380
+ "",
381
+ DRIVER_RULES,
382
+ ].join("\n");
383
+ }
384
+
385
+ function buildContextMessage(raw: Record<string, unknown>, sessionId: string, contextName: string): string {
386
+ const bc = (Array.isArray(raw.boundedContexts) ? (raw.boundedContexts as Record<string, unknown>[]) : [])
387
+ .find((x) => String(x.name ?? "") === contextName);
388
+ const need = (Array.isArray(bc?.entities) ? (bc!.entities as unknown[]) : []).map(String).filter(Boolean);
389
+ const done = Array.isArray(raw.entities) ? (raw.entities as Record<string, unknown>[])
390
+ .filter((e) => String(e.context ?? "") === contextName).map((e) => String(e.name ?? "")) : [];
391
+ const remaining = need.filter((n) => !done.includes(n));
392
+ return [
393
+ "Session " + sessionId + " — PER-CONTEXT PASS for \"" + contextName + "\"" +
394
+ (remaining.length > 0 ? " (still needed from strategic list: " + remaining.join(", ") + ")" : "") + ".",
395
+ "Enrich this ONE context: entities with entity-vs-VO justification, domain events with triage,",
396
+ "invariants, and this context's ubiquitous language. Up to 6 entities per message — if the",
397
+ "context has more, the driver will ask you to continue this same context.",
398
+ "",
399
+ "Reply with ONLY this JSON:",
400
+ '```json',
401
+ '{',
402
+ ' "boundedContexts": [{ "name": "' + contextName + '", "description": "...", "entities": ["Entity1"],',
403
+ ' "invariants": [{ "rule": "...", "illegalStatePrevented": "..." }] }],',
404
+ ' "entities": [',
405
+ ' { "name": "Entity1", "context": "' + contextName + '", "type": "entity|value-object|aggregate-root",',
406
+ ' "description": "...",',
407
+ ' "justification": { "identityPersists": true, "immutable": false, "equalByState": false } }',
408
+ ' ],',
409
+ ' "domainEvents": [',
410
+ ' { "name": "Event", "context": "' + contextName + '", "description": "...", "triggeredBy": "...",',
411
+ ' "triage": { "domainExpertCares": true, "crossesAggregateBoundary": false, "needsAudit": false } }',
412
+ ' ],',
413
+ ' "ubiquitousLanguage": [{ "term": "...", "definition": "...", "boundedContext": "' + contextName + '", "aliases": [], "examples": "" }]',
414
+ '}',
415
+ '```',
416
+ "",
417
+ DRIVER_RULES,
418
+ ].join("\n");
419
+ }
420
+
421
+ function buildPhaseMessage(kind: PhaseKind, raw: Record<string, unknown>, sessionId: string, contextName?: string): string {
422
+ if (kind === "strategic") return buildStrategicMessage(sessionId, String(raw.businessContext ?? ""));
423
+ if (kind === "requirements") return buildRequirementsMessage(raw, sessionId);
424
+ if (kind === "usecases") return buildUsecasesMessage(sessionId);
425
+ if (kind === "context") return buildContextMessage(raw, sessionId, contextName ?? "");
426
+ return "";
427
+ }
428
+
429
+ function moduleHasCode(cwd: string, moduleName: string): boolean {
430
+ const sanitized = (moduleName || "").toLowerCase().replace(/[^a-z0-9]/g, "");
431
+ if (!sanitized) return false;
432
+ const roots = ["src", "crates", "packages", "internal", "lib", "app", "modules"];
433
+ const codeFile = (n: string): boolean => /.*\.(java|kt|ts|tsx|js|jsx|rs|py|go)$/.test(n) && !/test|spec/i.test(n);
434
+ const walk = (dir: string, depth: number): boolean => {
435
+ if (depth > 6) return false;
436
+ let entries;
437
+ try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch { return false; }
438
+ for (const e of entries) {
439
+ if (["node_modules", "target", "dist", ".git"].includes(e.name)) continue;
440
+ const full = path.join(dir, e.name);
441
+ if (e.isFile() && codeFile(e.name)) return true;
442
+ if (e.isDirectory() && (e.name === sanitized || walk(full, depth + 1))) {
443
+ // entered the module dir: any code file below counts
444
+ if (e.name === sanitized && hasCodeBelow(full, depth)) return true;
445
+ }
446
+ }
447
+ return false;
448
+ };
449
+ const hasCodeBelow = (dir: string, depth: number): boolean => {
450
+ let entries;
451
+ try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch { return false; }
452
+ for (const e of entries) {
453
+ if (["node_modules", "target", "dist"].includes(e.name)) continue;
454
+ const full = path.join(dir, e.name);
455
+ if (e.isFile() && codeFile(e.name)) return true;
456
+ if (e.isDirectory() && depth < 6 && hasCodeBelow(full, depth + 1)) return true;
457
+ }
458
+ return false;
459
+ };
460
+ for (const r of roots) {
461
+ const base = path.join(cwd, r);
462
+ if (fs.existsSync(base) && walk(base, 0)) return true;
463
+ }
464
+ return false;
465
+ }
466
+
242
467
  function buildExplorationPrompt(context: string): string {
243
468
  return [
244
469
  "You are a Domain-Driven Design expert. Analyze the following business",
@@ -422,6 +647,14 @@ export default function (pi: ExtensionAPI) {
422
647
  return { content: [{ type: "text" as const, text: "ERROR: Failed to parse JSON: " + String(e) }] };
423
648
  }
424
649
 
650
+ // Phase-completion flags ride along on the model's JSON but are not part of
651
+ // the persisted schema — pull them off before merging so they never pollute
652
+ // the raw session, then fold them into protocolProgress below.
653
+ const flagsRc = parsedJson.requirementsComplete === true;
654
+ const flagsUc = parsedJson.usecasesComplete === true;
655
+ delete parsedJson.requirementsComplete;
656
+ delete parsedJson.usecasesComplete;
657
+
425
658
  // Incremental: merge into any previously saved structured session
426
659
  // (<session>.raw.json) so large domains can be saved in parts.
427
660
  const rawPath = path.join(ctx.cwd, ".pi", "domain", "exploration", sessionId + ".raw.json");
@@ -437,6 +670,19 @@ export default function (pi: ExtensionAPI) {
437
670
  parsedJson.status = String(parsedJson.status ?? "draft");
438
671
  parsedJson.createdAt = parsedJson.createdAt ?? new Date().toISOString();
439
672
 
673
+ // Self-driving protocol progress. The driver asks the model ONE phase at a
674
+ // time; these markers say which phases are finished. Stored separately so
675
+ // the model schema stays clean and id-keyed merges never disturb them.
676
+ const prevRawForProg = (() => {
677
+ try { return JSON.parse(fs.readFileSync(rawPath, "utf-8")) as Record<string, unknown>; }
678
+ catch { return {}; }
679
+ })();
680
+ const prevProg = (prevRawForProg.protocolProgress as Record<string, unknown>) ?? {};
681
+ parsedJson.protocolProgress = {
682
+ requirementsComplete: prevProg.requirementsComplete === true || flagsRc,
683
+ usecasesComplete: prevProg.usecasesComplete === true || flagsUc,
684
+ };
685
+
440
686
  // Derive aggregate roots from the entities table — never let the
441
687
  // rendered "## Aggregate Roots" contradict entities marked
442
688
  // aggregate-root (the protocol passes don't carry this field).
@@ -571,7 +817,27 @@ export default function (pi: ExtensionAPI) {
571
817
  }
572
818
 
573
819
  onUpdate?.({ content: [{ type: "text", text: "Domain exploration saved for session: " + sessionId }] });
574
- return { content: [{ type: "text" as const, text: "Domain exploration saved for " + sessionId + ". Next: /domain --architect-scaffold " + sessionId + " or review .pi/domain/exploration.md" }] };
820
+
821
+ // Self-driving: after every successful save, dispatch the next phase as a
822
+ // fresh small agent turn (or tell the model exploration is complete). If a
823
+ // phase needs more batches (requirements >5, context >6 entities) the same
824
+ // decision re-issues that phase with progress hints — never a reset.
825
+ const decision = nextPhaseDecision(parsedJson);
826
+ const savedNote = "Domain exploration saved for " + sessionId + ".";
827
+ if (decision.kind === "done") {
828
+ return { content: [{ type: "text" as const, text: savedNote + " Exploration content complete — run /domain --validate " + sessionId }] };
829
+ }
830
+ const label = decision.kind === "context" ? "context " + (decision.contextName ?? "") : decision.kind;
831
+ const nextMsg = buildPhaseMessage(decision.kind, parsedJson, sessionId, decision.contextName);
832
+ try {
833
+ pi.sendMessage(
834
+ { content: nextMsg, display: true },
835
+ { deliverAs: "followUp", triggerTurn: true },
836
+ );
837
+ return { content: [{ type: "text" as const, text: savedNote + " Next phase dispatched: " + label }] };
838
+ } catch {
839
+ return { content: [{ type: "text" as const, text: savedNote + "\n\nNEXT PHASE (" + label + "):\n" + nextMsg }] };
840
+ }
575
841
  },
576
842
  });
577
843
 
@@ -880,86 +1146,7 @@ export default function (pi: ExtensionAPI) {
880
1146
  // Inject the DDD analysis prompt as a follow-up message to the agent.
881
1147
  // Using sendMessage with triggerTurn=true causes the agent to process
882
1148
  // this as a new conversation turn, not as a command response.
883
- const analysisPrompt = [
884
- "Analyze this business domain with Domain-Driven Design. You MUST work in SMALL",
885
- "incremental steps — never produce the whole model in one message, or you will be",
886
- "truncated mid-JSON and lose work.",
887
- "",
888
- "Session: " + sessionId,
889
- "Business Context: " + sanitized,
890
- "",
891
- "STEP PROTOCOL (strict — one step per message, each message <= ~1,500 tokens of JSON):",
892
- "",
893
- "1. STRATEGIC PASS (this message): reply with ONLY this JSON, nothing else —",
894
- '```json',
895
- '{',
896
- ' "businessContext": "one-line summary of the domain",',
897
- ' "actors": [{ "name": "...", "description": "...", "interactions": "..." }],',
898
- ' "assumptions": [{ "assumption": "...", "impactIfWrong": "...", "mitigation": "..." }],',
899
- ' "boundedContexts": [',
900
- ' { "name": "...", "description": "...", "entities": ["..."],',
901
- ' "subdomainType": "core|supporting|generic", "subdomainRationale": "one line" }',
902
- ' ],',
903
- ' "contextMap": [{ "from": "A", "to": "B", "relationship": "partnership|shared-kernel|conformist|anticorruption-layer|open-host", "note": "..." }],',
904
- ' "openQuestions": "..."',
905
- '}',
906
- '```',
907
- "then call domain_save_result with { sessionId, responseJson: <that JSON> }. It merges;",
908
- "the file is saved for you. Do NOT also edit exploration.md by hand.",
909
- "",
910
- "1b. REQUIREMENTS PASS (next message): reply with ONLY this JSON, then call domain_save_result",
911
- "again (merge is by id — existing entries are untouched):",
912
- '```json',
913
- '{',
914
- ' "functionalRequirements": [',
915
- ' { "id": "FR-001", "requirement": "The system shall...", "priority": "critical|high|medium|low", "boundedContext": "ContextName" }',
916
- ' ],',
917
- ' "nonFunctionalRequirements": [',
918
- ' { "id": "NFR-001", "requirement": "The system shall...", "category": "performance|security|scalability|availability|maintainability", "target": "measurable target" }',
919
- ' ]',
920
- '}',
921
- '```',
922
- "",
923
- "1c. USE-CASES PASS (next message): reply with ONLY this JSON for ALL core contexts, then call",
924
- "domain_save_result (merge is by name+context):",
925
- '```json',
926
- '{',
927
- ' "applicationServices": [',
928
- ' { "name": "ApproveRequest", "context": "ApprovalWorkflow", "useCase": "what this application service orchestrates",',
929
- ' "commands": ["verify intent", "consume"], "aggregates": ["ApprovalTicket"], "mapsToFr": "FR-00x" }',
930
- ' ]',
931
- '}',
932
- '```',
933
- "",
934
- "2. PER-CONTEXT PASS (repeat for EACH core/tactical bounded context, one context per message):",
935
- "reply with ONLY this JSON for one context, then call domain_save_result again:",
936
- '```json',
937
- '{',
938
- ' "boundedContexts": [{ "name": "TheContext", "description": "...", "entities": ["Entity1"],',
939
- ' "invariants": [{ "rule": "...", "illegalStatePrevented": "..." }] }],',
940
- ' "entities": [',
941
- ' { "name": "Entity1", "context": "TheContext", "type": "entity|value-object|aggregate-root",',
942
- ' "description": "...",',
943
- ' "justification": { "identityPersists": true, "immutable": false, "equalByState": false } }',
944
- ' ],',
945
- ' "domainEvents": [',
946
- ' { "name": "Event", "context": "TheContext", "description": "...", "triggeredBy": "...",',
947
- ' "triage": { "domainExpertCares": true, "crossesAggregateBoundary": false, "needsAudit": false } }',
948
- ' ],',
949
- ' "ubiquitousLanguage": [{ "term": "...", "definition": "...", "boundedContext": "TheContext", "aliases": [], "examples": "" }]',
950
- '}',
951
- '```',
952
- "Supporting/generic contexts need no detail pass — the strategic entry is enough.",
953
- "",
954
- "RULES:",
955
- "- If a message of yours is cut off mid-JSON, continue from exactly where you stopped in the",
956
- " NEXT message (never restart the whole model).",
957
- "- Never emit the full model in one response and never exceed ~1,500 tokens of JSON per message.",
958
- "- HARD RULE: never narrate or reason in chat. No prose analysis, no relationship",
959
- " deliberation, no summaries — those burn your output budget and truncate the JSON.",
960
- " Emit ONLY the small JSON step for the current phase and stop.",
961
- "- After the last context, run /domain --validate " + sessionId + " and report the checks.",
962
- ].join("\n");
1149
+ const analysisPrompt = buildStrategicMessage(sessionId, sanitized);
963
1150
 
964
1151
 
965
1152
  try {
@@ -975,6 +1162,43 @@ export default function (pi: ExtensionAPI) {
975
1162
  return "Domain analysis dispatched to agent. Session: " + sessionId;
976
1163
  }
977
1164
 
1165
+ // /domain --resume <session-id> — re-dispatch the next unfinished phase from
1166
+ // persisted state. Truncation anywhere in the protocol is recoverable with
1167
+ // this: it never asks the model to reproduce saved content.
1168
+ if (trimmed.startsWith("--resume")) {
1169
+ const sessionId = trimmed.slice("--resume".length).trim().split(/\s+/)[0] || "";
1170
+ if (!sessionId) {
1171
+ ctx.ui.notify("Usage: /domain --resume <session-id>", "error");
1172
+ return "(domain command handled)";
1173
+ }
1174
+ const explorationDir = path.join(ctx.cwd, ".pi", "domain", "exploration");
1175
+ const rawPath = path.join(explorationDir, sessionId + ".raw.json");
1176
+ if (!fs.existsSync(rawPath)) {
1177
+ ctx.ui.notify("Session not found: " + sessionId + ". Run /domain --explore first.", "error");
1178
+ return "(domain command handled)";
1179
+ }
1180
+ let raw: Record<string, unknown> = {};
1181
+ try { raw = JSON.parse(fs.readFileSync(rawPath, "utf-8")) as Record<string, unknown>; }
1182
+ catch { ctx.ui.notify("Session raw state unreadable for " + sessionId, "error"); return "(domain command handled)"; }
1183
+ const decision = nextPhaseDecision(raw);
1184
+ if (decision.kind === "done") {
1185
+ ctx.ui.notify("Exploration content is complete — run /domain --validate " + sessionId, "info");
1186
+ return "(domain command handled)";
1187
+ }
1188
+ const label = decision.kind === "context" ? "context " + decision.contextName : decision.kind;
1189
+ ctx.ui.notify("Resumed " + sessionId + " at phase: " + label, "success");
1190
+ const msg = buildPhaseMessage(decision.kind, raw, sessionId, decision.contextName);
1191
+ try {
1192
+ pi.sendMessage(
1193
+ { content: msg, display: true },
1194
+ { deliverAs: "followUp", triggerTurn: true },
1195
+ );
1196
+ return "Resumed " + sessionId + " at phase: " + label;
1197
+ } catch {
1198
+ return msg;
1199
+ }
1200
+ }
1201
+
978
1202
  // /domain --architect-scaffold <session-id>
979
1203
  // /domain --architect-scaffold <session-id>
980
1204
  if (trimmed.startsWith("--architect-scaffold")) {
@@ -1058,7 +1282,7 @@ export default function (pi: ExtensionAPI) {
1058
1282
  let scaffoldError = "";
1059
1283
 
1060
1284
  try {
1061
- const cmd = "guardian-framework domain scaffold " + sessionId + " 2>&1";
1285
+ const cmd = "guardian-framework domain scaffold " + sessionId + (forceScaffold ? " --force" : "") + " 2>&1";
1062
1286
  const shellResult = await shellExec(ctx, cmd);
1063
1287
  if (shellResult.exitCode === 0) {
1064
1288
  const outputLines = shellResult.stdout.split("\n");
@@ -1281,7 +1505,9 @@ export default function (pi: ExtensionAPI) {
1281
1505
  }
1282
1506
  const preserved: string[] = [];
1283
1507
  const moduleFiles = sessionModules.filter((f) => {
1284
- if (fs.existsSync(path.join(modulesDir, f)) && !forceEnrich) {
1508
+ const exists = fs.existsSync(path.join(modulesDir, f));
1509
+ const implemented = moduleHasCode(ctx.cwd, f.replace(/\.md$/, ""));
1510
+ if (exists && implemented && !forceEnrich) {
1285
1511
  preserved.push(f);
1286
1512
  return false;
1287
1513
  }
@@ -1,5 +1,5 @@
1
1
  {
2
- "timestamp": "2026-09-06T13:11:45Z",
2
+ "timestamp": "2026-09-06T14:57:05Z",
3
3
  "mode": "all",
4
4
  "stages_run": [],
5
5
  "summary": {