gentle-pi 0.7.0 → 0.9.2
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/assets/agents/sdd-apply.md +20 -5
- package/assets/agents/sdd-archive.md +15 -3
- package/assets/agents/sdd-design.md +11 -3
- package/assets/agents/sdd-explore.md +12 -4
- package/assets/agents/sdd-init.md +11 -3
- package/assets/agents/sdd-onboard.md +11 -3
- package/assets/agents/sdd-proposal.md +12 -4
- package/assets/agents/sdd-spec.md +11 -3
- package/assets/agents/sdd-status.md +8 -3
- package/assets/agents/sdd-sync.md +13 -3
- package/assets/agents/sdd-tasks.md +11 -3
- package/assets/agents/sdd-verify.md +17 -4
- package/assets/orchestrator.md +26 -5
- package/assets/support/sdd-status-contract.md +12 -4
- package/extensions/gentle-ai.ts +3 -0
- package/lib/sdd-preflight.ts +68 -2
- package/lib/sdd-status.ts +151 -17
- package/package.json +1 -1
- package/tests/sdd-preflight.test.ts +113 -0
- package/tests/sdd-status.test.ts +320 -0
package/tests/sdd-status.test.ts
CHANGED
|
@@ -5,9 +5,12 @@ import { tmpdir } from "node:os";
|
|
|
5
5
|
import { dirname, join } from "node:path";
|
|
6
6
|
import test from "node:test";
|
|
7
7
|
import {
|
|
8
|
+
isNonAuthoritativeStatus,
|
|
8
9
|
listActiveOpenSpecChanges,
|
|
9
10
|
parseSddStatusCommandArgs,
|
|
11
|
+
renderNativeSddPhasePrompt,
|
|
10
12
|
renderPhaseInstructions,
|
|
13
|
+
renderSddDispatcherMarkdown,
|
|
11
14
|
renderSddStatusMarkdown,
|
|
12
15
|
resolveSddStatus,
|
|
13
16
|
} from "../lib/sdd-status.ts";
|
|
@@ -279,6 +282,76 @@ test("renderSddStatusMarkdown includes structured JSON", async () => {
|
|
|
279
282
|
assert.match(markdown, /"schemaName": "gentle-pi.sdd-status"/);
|
|
280
283
|
});
|
|
281
284
|
|
|
285
|
+
test("resolveSddStatus with artifactStore engram returns non-authoritative status without disk scan", async () => {
|
|
286
|
+
const cwd = await workspace();
|
|
287
|
+
// No openspec directory — simulates an engram-only session
|
|
288
|
+
|
|
289
|
+
const status = resolveSddStatus({ cwd, artifactStore: "engram", changeName: "my-change" });
|
|
290
|
+
|
|
291
|
+
assert.equal(status.artifactStore, "engram");
|
|
292
|
+
assert.equal(status.changeName, "my-change");
|
|
293
|
+
assert.deepEqual(status.blockedReasons, []);
|
|
294
|
+
assert.equal(status.nextRecommended, "resolve-via-engram");
|
|
295
|
+
assert.equal(status.dependencies.apply, "not_applicable");
|
|
296
|
+
assert.equal(status.applyState, "not_applicable");
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
test("resolveSddStatus with artifactStore none returns non-authoritative status without disk scan", async () => {
|
|
300
|
+
const cwd = await workspace();
|
|
301
|
+
// No openspec directory
|
|
302
|
+
|
|
303
|
+
const status = resolveSddStatus({ cwd, artifactStore: "none", changeName: "my-change" });
|
|
304
|
+
|
|
305
|
+
assert.equal(status.artifactStore, "none");
|
|
306
|
+
assert.deepEqual(status.blockedReasons, []);
|
|
307
|
+
assert.equal(status.nextRecommended, "resolve-via-engram");
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
test("resolveSddStatus with artifactStore both uses disk scan and reflects store", async () => {
|
|
311
|
+
const cwd = await workspace();
|
|
312
|
+
seedChange(cwd);
|
|
313
|
+
|
|
314
|
+
const status = resolveSddStatus({ cwd, artifactStore: "both", changeName: "add-auth" });
|
|
315
|
+
|
|
316
|
+
assert.equal(status.artifactStore, "both");
|
|
317
|
+
assert.equal(status.changeName, "add-auth");
|
|
318
|
+
assert.notEqual(status.nextRecommended, "resolve-via-engram");
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
test("resolveSddStatus with undefined store and existing openspec dir defaults to openspec and blocks", async () => {
|
|
322
|
+
const cwd = await workspace();
|
|
323
|
+
// Create openspec/ directory to signal an openspec workspace
|
|
324
|
+
mkdirSync(join(cwd, "openspec", "changes"), { recursive: true });
|
|
325
|
+
|
|
326
|
+
const status = resolveSddStatus({ cwd });
|
|
327
|
+
|
|
328
|
+
// openspec workspace with no active changes → blocked (back-compat)
|
|
329
|
+
assert.equal(status.artifactStore, "openspec");
|
|
330
|
+
assert.match(status.blockedReasons[0] ?? "", /No active SDD changes/);
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
test("resolveSddStatus with undefined store and NO openspec dir returns non-authoritative status", async () => {
|
|
334
|
+
const cwd = await workspace();
|
|
335
|
+
// No openspec directory at all — unknown store, no disk evidence
|
|
336
|
+
|
|
337
|
+
const status = resolveSddStatus({ cwd });
|
|
338
|
+
|
|
339
|
+
// Safety net: should not emit the openspec false-block
|
|
340
|
+
assert.equal(status.artifactStore, "none");
|
|
341
|
+
assert.deepEqual(status.blockedReasons, []);
|
|
342
|
+
assert.equal(status.nextRecommended, "resolve-via-engram");
|
|
343
|
+
assert.equal(status.applyState, "not_applicable");
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
test("resolveSddStatus non-authoritative status has neutral planningHome (no misleading openspec path)", async () => {
|
|
347
|
+
const cwd = await workspace();
|
|
348
|
+
|
|
349
|
+
const status = resolveSddStatus({ cwd, artifactStore: "engram", changeName: "fix-auth" });
|
|
350
|
+
|
|
351
|
+
assert.equal(status.planningHome.changesDir, "");
|
|
352
|
+
assert.equal(status.planningHome.root, status.actionContext.workspaceRoot);
|
|
353
|
+
});
|
|
354
|
+
|
|
282
355
|
test("parseSddStatusCommandArgs extracts change and json flag", () => {
|
|
283
356
|
assert.deepEqual(parseSddStatusCommandArgs("add-auth --json"), {
|
|
284
357
|
changeName: "add-auth",
|
|
@@ -289,3 +362,250 @@ test("parseSddStatusCommandArgs extracts change and json flag", () => {
|
|
|
289
362
|
json: true,
|
|
290
363
|
});
|
|
291
364
|
});
|
|
365
|
+
|
|
366
|
+
test("resolveSddStatus with artifactStore both and NO openspec dir returns non-authoritative status", async () => {
|
|
367
|
+
const cwd = await workspace();
|
|
368
|
+
// No openspec directory — both store without disk backing is non-authoritative
|
|
369
|
+
|
|
370
|
+
const status = resolveSddStatus({ cwd, artifactStore: "both", changeName: "my-change" });
|
|
371
|
+
|
|
372
|
+
assert.equal(status.artifactStore, "both");
|
|
373
|
+
assert.equal(status.changeName, "my-change");
|
|
374
|
+
assert.deepEqual(status.blockedReasons, []);
|
|
375
|
+
assert.equal(status.nextRecommended, "resolve-via-engram");
|
|
376
|
+
assert.equal(status.applyState, "not_applicable");
|
|
377
|
+
assert.equal(status.dependencies.apply, "not_applicable");
|
|
378
|
+
assert.equal(status.dependencies.archive, "not_applicable");
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
test("resolveSddStatus with artifactStore both and existing openspec dir runs authoritative disk scan", async () => {
|
|
382
|
+
const cwd = await workspace();
|
|
383
|
+
seedChange(cwd);
|
|
384
|
+
|
|
385
|
+
const status = resolveSddStatus({ cwd, artifactStore: "both", changeName: "add-auth" });
|
|
386
|
+
|
|
387
|
+
assert.equal(status.artifactStore, "both");
|
|
388
|
+
assert.equal(status.changeName, "add-auth");
|
|
389
|
+
assert.notEqual(status.nextRecommended, "resolve-via-engram");
|
|
390
|
+
assert.equal(status.artifacts.proposal, "done");
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
// Renamed: previously "returns true only when nextRecommended is resolve-via-engram" — now
|
|
394
|
+
// explicitly asserts BOTH the typed isNonAuthoritative field and the sentinel together.
|
|
395
|
+
test("isNonAuthoritativeStatus reads typed isNonAuthoritative field and matches resolve-via-engram sentinel", async () => {
|
|
396
|
+
const cwd = await workspace();
|
|
397
|
+
|
|
398
|
+
const engram = resolveSddStatus({ cwd, artifactStore: "engram", changeName: "x" });
|
|
399
|
+
assert.equal(engram.isNonAuthoritative, true);
|
|
400
|
+
assert.equal(isNonAuthoritativeStatus(engram), true);
|
|
401
|
+
assert.equal(engram.nextRecommended, "resolve-via-engram");
|
|
402
|
+
|
|
403
|
+
const none = resolveSddStatus({ cwd, artifactStore: "none", changeName: "x" });
|
|
404
|
+
assert.equal(none.isNonAuthoritative, true);
|
|
405
|
+
assert.equal(isNonAuthoritativeStatus(none), true);
|
|
406
|
+
assert.equal(none.nextRecommended, "resolve-via-engram");
|
|
407
|
+
|
|
408
|
+
const bothWithoutOpenspec = resolveSddStatus({ cwd, artifactStore: "both", changeName: "x" });
|
|
409
|
+
assert.equal(bothWithoutOpenspec.isNonAuthoritative, true);
|
|
410
|
+
assert.equal(isNonAuthoritativeStatus(bothWithoutOpenspec), true);
|
|
411
|
+
assert.equal(bothWithoutOpenspec.nextRecommended, "resolve-via-engram");
|
|
412
|
+
|
|
413
|
+
seedChange(cwd);
|
|
414
|
+
const bothWithOpenspec = resolveSddStatus({ cwd, artifactStore: "both", changeName: "add-auth" });
|
|
415
|
+
assert.equal(bothWithOpenspec.isNonAuthoritative, false);
|
|
416
|
+
assert.equal(isNonAuthoritativeStatus(bothWithOpenspec), false);
|
|
417
|
+
assert.notEqual(bothWithOpenspec.nextRecommended, "resolve-via-engram");
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
// Fix 4 item 4 — isNonAuthoritative boolean is set correctly on the typed field
|
|
421
|
+
test("isNonAuthoritative boolean field is set correctly across all store/disk combinations", async () => {
|
|
422
|
+
const cwd = await workspace();
|
|
423
|
+
|
|
424
|
+
// engram → non-authoritative
|
|
425
|
+
const engram = resolveSddStatus({ cwd, artifactStore: "engram", changeName: "x" });
|
|
426
|
+
assert.equal(engram.isNonAuthoritative, true);
|
|
427
|
+
|
|
428
|
+
// none → non-authoritative
|
|
429
|
+
const none = resolveSddStatus({ cwd, artifactStore: "none", changeName: "x" });
|
|
430
|
+
assert.equal(none.isNonAuthoritative, true);
|
|
431
|
+
|
|
432
|
+
// both without openspec/ → non-authoritative
|
|
433
|
+
const bothWithout = resolveSddStatus({ cwd, artifactStore: "both", changeName: "x" });
|
|
434
|
+
assert.equal(bothWithout.isNonAuthoritative, true);
|
|
435
|
+
|
|
436
|
+
// both WITH openspec/ and seeded change → authoritative
|
|
437
|
+
seedChange(cwd);
|
|
438
|
+
const bothWith = resolveSddStatus({ cwd, artifactStore: "both", changeName: "add-auth" });
|
|
439
|
+
assert.equal(bothWith.isNonAuthoritative, false);
|
|
440
|
+
|
|
441
|
+
// openspec (default disk scan, seeded) → authoritative
|
|
442
|
+
const openspec = resolveSddStatus({ cwd, artifactStore: "openspec", changeName: "add-auth" });
|
|
443
|
+
assert.equal(openspec.isNonAuthoritative, false);
|
|
444
|
+
});
|
|
445
|
+
|
|
446
|
+
// Fix 4 item 1 — both + openspec/ dir present + change NOT on disk → non-authoritative
|
|
447
|
+
test("resolveSddStatus with artifactStore both, openspec dir present but change not on disk returns non-authoritative", async () => {
|
|
448
|
+
const cwd = await workspace();
|
|
449
|
+
// Create an openspec/changes dir with a different change — not the requested one
|
|
450
|
+
mkdirSync(join(cwd, "openspec", "changes", "other-change"), { recursive: true });
|
|
451
|
+
|
|
452
|
+
const status = resolveSddStatus({ cwd, artifactStore: "both", changeName: "missing-change" });
|
|
453
|
+
|
|
454
|
+
assert.equal(status.isNonAuthoritative, true);
|
|
455
|
+
assert.equal(status.nextRecommended, "resolve-via-engram");
|
|
456
|
+
assert.deepEqual(status.blockedReasons, []);
|
|
457
|
+
assert.equal(status.applyState, "not_applicable");
|
|
458
|
+
assert.equal(status.artifactStore, "both");
|
|
459
|
+
// Must NOT be treated as blocked
|
|
460
|
+
assert.notEqual(status.applyState, "blocked");
|
|
461
|
+
});
|
|
462
|
+
|
|
463
|
+
// Fix 4 item 2 — strengthen existing both-with-openspec-and-seeded-change test
|
|
464
|
+
test("resolveSddStatus with artifactStore both, openspec dir present and change on disk is authoritative", async () => {
|
|
465
|
+
const cwd = await workspace();
|
|
466
|
+
seedChange(cwd);
|
|
467
|
+
|
|
468
|
+
const status = resolveSddStatus({ cwd, artifactStore: "both", changeName: "add-auth" });
|
|
469
|
+
|
|
470
|
+
assert.equal(status.artifactStore, "both");
|
|
471
|
+
assert.equal(status.changeName, "add-auth");
|
|
472
|
+
// Must be authoritative
|
|
473
|
+
assert.equal(isNonAuthoritativeStatus(status), false);
|
|
474
|
+
assert.equal(status.isNonAuthoritative, false);
|
|
475
|
+
// Must not be not_applicable — real disk scan ran
|
|
476
|
+
assert.notEqual(status.applyState, "not_applicable");
|
|
477
|
+
assert.notEqual(status.nextRecommended, "resolve-via-engram");
|
|
478
|
+
assert.equal(status.artifacts.proposal, "done");
|
|
479
|
+
});
|
|
480
|
+
|
|
481
|
+
// Fix 4 item 3 — pure openspec store + change not found STILL blocks (guard against over-broadening Fix 2)
|
|
482
|
+
test("resolveSddStatus with artifactStore openspec and change not found still blocks", async () => {
|
|
483
|
+
const cwd = await workspace();
|
|
484
|
+
// Create openspec dir with a different change — simulate openspec store with no matching change
|
|
485
|
+
mkdirSync(join(cwd, "openspec", "changes", "other-change"), { recursive: true });
|
|
486
|
+
|
|
487
|
+
const status = resolveSddStatus({ cwd, artifactStore: "openspec", changeName: "nonexistent" });
|
|
488
|
+
|
|
489
|
+
// Must block, not return non-authoritative
|
|
490
|
+
assert.equal(status.isNonAuthoritative, false);
|
|
491
|
+
assert.match(status.blockedReasons.join("\n"), /Active change not found/);
|
|
492
|
+
assert.equal(status.applyState, "blocked");
|
|
493
|
+
assert.notEqual(status.nextRecommended, "resolve-via-engram");
|
|
494
|
+
});
|
|
495
|
+
|
|
496
|
+
test("renderSddDispatcherMarkdown for both-without-openspec does NOT render Ready", async () => {
|
|
497
|
+
const cwd = await workspace();
|
|
498
|
+
// No openspec directory — both store is non-authoritative
|
|
499
|
+
|
|
500
|
+
const status = resolveSddStatus({ cwd, artifactStore: "both", changeName: "fix-x" });
|
|
501
|
+
const markdown = renderSddDispatcherMarkdown(status);
|
|
502
|
+
|
|
503
|
+
assert.doesNotMatch(markdown, /### Ready/);
|
|
504
|
+
assert.match(markdown, /resolve via Engram/i);
|
|
505
|
+
});
|
|
506
|
+
|
|
507
|
+
test("renderNativeSddPhasePrompt for both-without-openspec emits non-authoritative line", async () => {
|
|
508
|
+
const cwd = await workspace();
|
|
509
|
+
|
|
510
|
+
const status = resolveSddStatus({ cwd, artifactStore: "both", changeName: "fix-x" });
|
|
511
|
+
const prompt = renderNativeSddPhasePrompt(status, "apply");
|
|
512
|
+
|
|
513
|
+
assert.match(prompt, /non-authoritative/);
|
|
514
|
+
assert.doesNotMatch(prompt, /deterministically/);
|
|
515
|
+
});
|
|
516
|
+
|
|
517
|
+
test("renderPhaseInstructions for not_applicable applyState emits neutral line", async () => {
|
|
518
|
+
const cwd = await workspace();
|
|
519
|
+
|
|
520
|
+
const status = resolveSddStatus({ cwd, artifactStore: "engram", changeName: "fix-x" });
|
|
521
|
+
const instructions = renderPhaseInstructions(status);
|
|
522
|
+
|
|
523
|
+
assert.match(instructions.apply.join("\n"), /Readiness is resolved from Engram/);
|
|
524
|
+
assert.match(instructions.archive.join("\n"), /Readiness is resolved from Engram/);
|
|
525
|
+
});
|
|
526
|
+
|
|
527
|
+
// Fix 4 item 1 — both + openspec/ + ZERO changes + no changeName → non-authoritative
|
|
528
|
+
test("resolveSddStatus both + openspec/ dir + zero active changes + no changeName returns non-authoritative", async () => {
|
|
529
|
+
const cwd = await workspace();
|
|
530
|
+
// openspec/ dir exists but holds no active changes (only the changes/ subdir)
|
|
531
|
+
mkdirSync(join(cwd, "openspec", "changes"), { recursive: true });
|
|
532
|
+
|
|
533
|
+
const status = resolveSddStatus({ cwd, artifactStore: "both" });
|
|
534
|
+
|
|
535
|
+
assert.equal(status.isNonAuthoritative, true);
|
|
536
|
+
assert.equal(status.nextRecommended, "resolve-via-engram");
|
|
537
|
+
assert.deepEqual(status.blockedReasons, []);
|
|
538
|
+
assert.equal(status.artifactStore, "both");
|
|
539
|
+
assert.equal(status.applyState, "not_applicable");
|
|
540
|
+
assert.equal(status.dependencies.apply, "not_applicable");
|
|
541
|
+
assert.equal(status.dependencies.archive, "not_applicable");
|
|
542
|
+
// Must NOT be treated as blocked
|
|
543
|
+
assert.notEqual(status.applyState, "blocked");
|
|
544
|
+
});
|
|
545
|
+
|
|
546
|
+
// Fix 4 item 2 — both + openspec/ + MULTIPLE changes + no changeName → authoritative select-change
|
|
547
|
+
test("resolveSddStatus both + openspec/ dir + multiple active changes + no changeName stays authoritative", async () => {
|
|
548
|
+
const cwd = await workspace();
|
|
549
|
+
mkdirSync(join(cwd, "openspec", "changes", "alpha"), { recursive: true });
|
|
550
|
+
mkdirSync(join(cwd, "openspec", "changes", "beta"), { recursive: true });
|
|
551
|
+
|
|
552
|
+
const status = resolveSddStatus({ cwd, artifactStore: "both" });
|
|
553
|
+
|
|
554
|
+
// Authoritative ambiguous-selection behavior must be preserved
|
|
555
|
+
assert.equal(status.isNonAuthoritative, false);
|
|
556
|
+
assert.match(status.blockedReasons.join("\n"), /ambiguous/);
|
|
557
|
+
assert.notEqual(status.nextRecommended, "resolve-via-engram");
|
|
558
|
+
});
|
|
559
|
+
|
|
560
|
+
// Fix 4 item 3 — both + openspec/ + ONE resolvable change → authoritative
|
|
561
|
+
test("resolveSddStatus both + openspec/ dir + exactly one active change is authoritative", async () => {
|
|
562
|
+
const cwd = await workspace();
|
|
563
|
+
seedChange(cwd);
|
|
564
|
+
|
|
565
|
+
// No changeName supplied — should auto-select the single change
|
|
566
|
+
const status = resolveSddStatus({ cwd, artifactStore: "both" });
|
|
567
|
+
|
|
568
|
+
assert.equal(status.isNonAuthoritative, false);
|
|
569
|
+
assert.equal(status.changeName, "add-auth");
|
|
570
|
+
assert.equal(status.artifactStore, "both");
|
|
571
|
+
assert.notEqual(status.applyState, "not_applicable");
|
|
572
|
+
assert.notEqual(status.nextRecommended, "resolve-via-engram");
|
|
573
|
+
assert.equal(status.artifacts.proposal, "done");
|
|
574
|
+
});
|
|
575
|
+
|
|
576
|
+
// Fix 4 item 4 — pure openspec + zero/missing change STILL blocks (guard against over-broadening)
|
|
577
|
+
test("resolveSddStatus openspec + zero active changes still blocks", async () => {
|
|
578
|
+
const cwd = await workspace();
|
|
579
|
+
mkdirSync(join(cwd, "openspec", "changes"), { recursive: true });
|
|
580
|
+
|
|
581
|
+
const status = resolveSddStatus({ cwd, artifactStore: "openspec" });
|
|
582
|
+
|
|
583
|
+
assert.equal(status.isNonAuthoritative, false);
|
|
584
|
+
assert.match(status.blockedReasons.join("\n"), /No active SDD changes/);
|
|
585
|
+
assert.equal(status.applyState, "blocked");
|
|
586
|
+
assert.notEqual(status.nextRecommended, "resolve-via-engram");
|
|
587
|
+
});
|
|
588
|
+
|
|
589
|
+
test("resolveSddStatus openspec + named change missing still blocks", async () => {
|
|
590
|
+
const cwd = await workspace();
|
|
591
|
+
mkdirSync(join(cwd, "openspec", "changes", "other-change"), { recursive: true });
|
|
592
|
+
|
|
593
|
+
const status = resolveSddStatus({ cwd, artifactStore: "openspec", changeName: "nonexistent" });
|
|
594
|
+
|
|
595
|
+
assert.equal(status.isNonAuthoritative, false);
|
|
596
|
+
assert.match(status.blockedReasons.join("\n"), /Active change not found/);
|
|
597
|
+
assert.equal(status.applyState, "blocked");
|
|
598
|
+
assert.notEqual(status.nextRecommended, "resolve-via-engram");
|
|
599
|
+
});
|
|
600
|
+
|
|
601
|
+
// Fix 4 render test — non-authoritative both status → dispatcher shows "both" not "Engram or none"
|
|
602
|
+
test("renderSddDispatcherMarkdown for non-authoritative both status shows artifact store 'both'", async () => {
|
|
603
|
+
const cwd = await workspace();
|
|
604
|
+
|
|
605
|
+
const status = resolveSddStatus({ cwd, artifactStore: "both", changeName: "fix-x" });
|
|
606
|
+
const markdown = renderSddDispatcherMarkdown(status);
|
|
607
|
+
|
|
608
|
+
assert.match(markdown, /artifact store: both/);
|
|
609
|
+
assert.doesNotMatch(markdown, /Engram or none/);
|
|
610
|
+
assert.match(markdown, /resolve via Engram/i);
|
|
611
|
+
});
|