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.
|
|
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.
|
|
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
|
@@ -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
|
-
|
|
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 = "
|
|
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");
|