gentle-pi 2.2.0 → 2.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +76 -79
- package/assets/agents/review-readability.md +0 -2
- package/assets/agents/review-reliability.md +0 -2
- package/assets/agents/review-resilience.md +0 -2
- package/assets/agents/review-risk.md +0 -2
- package/assets/agents/sdd-apply.md +5 -3
- package/assets/agents/sdd-explore.md +1 -0
- package/assets/agents/sdd-proposal.md +2 -0
- package/assets/agents/sdd-research.md +54 -0
- package/assets/agents/sdd-status.md +4 -4
- package/assets/agents/sdd-tasks.md +2 -3
- package/assets/agents/sdd-verify.md +21 -1
- package/assets/chains/sdd-full.chain.md +1 -1
- package/assets/chains/sdd-verify.chain.md +1 -1
- package/assets/orchestrator-delegation.md +56 -249
- package/assets/orchestrator-memory.md +2 -0
- package/assets/orchestrator.md +20 -40
- package/assets/sdd-orchestrator-workflow.md +30 -37
- package/assets/support/sdd-status-contract.md +7 -7
- package/contracts/review-integration/v1/schemas/transition-execution.schema.json +42 -0
- package/contracts/review-integration/v2/schemas/last-event-closure.schema.json +66 -0
- package/contracts/review-integration/v2/schemas/opencode-provider-role.schema.json +14 -0
- package/contracts/review-provider-contract-mirror/provider-contract.lock.json +8 -7
- package/contracts/review-provider-contract-mirror/{v1.1.0 → v1.2.0}/bundle/README.md +10 -0
- package/contracts/review-provider-contract-mirror/v1.2.0/bundle/manifest.json +74 -0
- package/contracts/review-provider-contract-mirror/v1.2.0/bundle/orchestration/pi.md +53 -0
- package/contracts/review-provider-contract-mirror/v1.2.0/bundle/schemas/targeted-validator.schema.json +1 -0
- package/contracts/review-provider-contract-mirror/{v1.1.0 → v1.2.0}/generated/provider-capabilities.baseline.json +9 -2
- package/contracts/review-provider-contract-mirror/{v1.1.0 → v1.2.0}/generated/provider-roles.baseline.json +2 -2
- package/docs/native-authority-architecture.md +9 -11
- package/docs/review-integration.md +27 -373
- package/extensions/ask-user-choice.ts +151 -0
- package/extensions/gentle-ai.ts +2349 -3785
- package/extensions/quiet-tools.ts +515 -32
- package/extensions/sdd-init.ts +4 -8
- package/lib/agent-home.ts +8 -0
- package/lib/gentle-ai-renderer.ts +70 -0
- package/lib/model-routing-authority.ts +133 -0
- package/lib/native-review-cli.ts +387 -940
- package/lib/opaque-pi-reviewer-adapter.ts +284 -0
- package/lib/provider-contract-bundle.ts +88 -6
- package/lib/review-candidate-view.ts +341 -132
- package/lib/review-host-relay.ts +324 -96
- package/lib/review-integration-v2.ts +868 -307
- package/lib/review-last-event-controller.ts +35 -0
- package/lib/sdd-preflight.ts +179 -68
- package/lib/sdd-status.ts +66 -111
- package/lib/terminal-theme.ts +1 -1
- package/package.json +83 -82
- package/runtime/gentle-ai-binary.mjs +1 -1
- package/runtime/native-review-cli.mjs +337 -890
- package/runtime/review-integration-v2.mjs +816 -255
- package/runtime/review-relay-contract.mjs +1 -1
- package/scripts/{build-git-commit-transaction-runner.mjs → build-runtime-modules.mjs} +4 -5
- package/scripts/gentle-ai-installer.mjs +75 -22
- package/scripts/maintainer/provider-relay-matrix.mjs +195 -11
- package/scripts/test-packed-runner.mjs +4 -7
- package/scripts/verify-package-files.mjs +23 -23
- package/skills/_shared/review-ledger-contract.md +14 -14
- package/skills/chained-pr/SKILL.md +3 -0
- package/skills/cognitive-doc-design/SKILL.md +1 -1
- package/skills/comment-writer/SKILL.md +1 -1
- package/skills/gentle-ai/SKILL.md +7 -74
- package/skills/issue-creation/SKILL.md +53 -93
- package/skills/judgment-day/SKILL.md +5 -7
- package/skills/rdd-defect-workflow/SKILL.md +3 -3
- package/skills/release/SKILL.md +3 -3
- package/skills/skill-registry/SKILL.md +1 -1
- package/skills/work-unit-commits/SKILL.md +3 -1
- package/tests/artifact-language.test.ts +24 -11
- package/tests/ask-user-choice.test.ts +264 -0
- package/tests/codegraph-tools.test.ts +3 -3
- package/tests/crosslane/cross-lane.mjs +15 -1168
- package/tests/delegated-key-learnings-contract.test.ts +8 -6
- package/tests/devbinary/native-review-parity.devtest.ts +167 -250
- package/tests/devbinary/pi-host-relay.devtest.ts +867 -0
- package/tests/fixtures/devbinary/last-event-capture-correction-plan.captured.json +10 -0
- package/tests/fixtures/devbinary/last-event-capture-refuter-approved.captured.json +20 -0
- package/tests/fixtures/devbinary/last-event-capture-refuter-correction-required.captured.json +26 -0
- package/tests/fixtures/devbinary/last-event-capture-result-approved.captured.json +8 -0
- package/tests/fixtures/devbinary/last-event-capture-result-correction-required.captured.json +26 -0
- package/tests/fixtures/devbinary/last-event-capture-validation-approved.captured.json +8 -0
- package/tests/fixtures/devbinary/last-event-closure.provenance.md +13 -0
- package/tests/fixtures/devbinary/review-acknowledged-v1.captured.json +9 -0
- package/tests/fixtures/devbinary/review-acknowledged.provenance.md +31 -0
- package/tests/fixtures/devbinary/start-v3-zero-lens-closed.captured.json +21 -0
- package/tests/fixtures/native-review-cli/v2.5.0-rc.1/PROVENANCE.txt +15 -0
- package/tests/fixtures/provider-contract-bundle/v1.2.0/README.md +22 -0
- package/{contracts/review-provider-contract-mirror/v1.1.0/bundle → tests/fixtures/provider-contract-bundle/v1.2.0}/manifest.json +11 -2
- package/tests/fixtures/provider-contract-bundle/v1.2.0/orchestration/pi.md +97 -0
- package/tests/fixtures/provider-contract-bundle/v1.2.0/schemas/lens.schema.json +16 -0
- package/tests/fixtures/provider-contract-bundle/v1.2.0/schemas/refuter.schema.json +1 -0
- package/tests/fixtures/provider-contract-bundle/v1.2.0/vectors/lens.json +1 -0
- package/tests/fixtures/provider-contract-bundle/v1.2.0/vectors/refuter.json +1 -0
- package/tests/fixtures/provider-contract-bundle/v1.2.0/vectors/targeted-validator.json +1 -0
- package/tests/gentle-ai-binary.test.ts +83 -13
- package/tests/gentle-ai-dev-binary.test.ts +51 -228
- package/tests/gentle-ai-installer.test.ts +137 -47
- package/tests/gentle-ai.test.ts +821 -131
- package/tests/gentle-theme.test.ts +133 -0
- package/tests/issue-creation-skill.test.ts +103 -0
- package/tests/maintainer/provider-relay.maintest.ts +340 -4
- package/tests/model-routing-authority.test.ts +257 -0
- package/tests/native-review-capability-contract.test.ts +45 -1
- package/tests/native-review-cli.test.ts +586 -962
- package/tests/native-review-consent.test.ts +150 -66
- package/tests/native-review-parity-runtime.test.ts +80 -359
- package/tests/native-review-parity.test.ts +674 -925
- package/tests/opaque-pi-reviewer-adapter.test.ts +266 -0
- package/tests/orchestrator-budget.test.ts +132 -88
- package/tests/orchestrator-rdd-ownership.test.ts +103 -0
- package/tests/package-manifest.test.ts +152 -36
- package/tests/provider-contract-bundle.test.ts +76 -0
- package/tests/provider-contract-mirror.test.ts +19 -0
- package/tests/provider-defect-handoff.test.ts +15 -118
- package/tests/quiet-tool-rendering.test.ts +1055 -28
- package/tests/review-agent-end-preflight.test.ts +408 -0
- package/tests/review-authority-recovery-docs.test.ts +1 -2
- package/tests/review-candidate-view.test.ts +409 -3
- package/tests/review-compact-contract.test.ts +4 -30
- package/tests/review-contract-prompt.test.ts +139 -0
- package/tests/review-controller-lock-status.test.ts +2 -2
- package/tests/review-controller-native-recovery.test.ts +363 -1450
- package/tests/review-controller-native-routing.test.ts +1251 -5642
- package/tests/review-controller-retired-ops.test.ts +1 -1
- package/tests/review-controller-workspace-root.test.ts +222 -71
- package/tests/review-controller.test.ts +26 -816
- package/tests/review-corrected-finalize-binding.test.ts +112 -153
- package/tests/review-dispatch-hydration-gap.test.ts +1 -53
- package/tests/review-gate.test.ts +0 -45
- package/tests/review-host-relay-restart-parity.test.ts +360 -0
- package/tests/review-host-relay-routing.test.ts +328 -91
- package/tests/review-host-relay.test.ts +436 -14
- package/tests/review-integration-v2-forward.test.ts +623 -553
- package/tests/review-integration-v2.test.ts +239 -165
- package/tests/review-last-event-closure.test.ts +408 -0
- package/tests/review-ledger-contract.test.ts +97 -35
- package/tests/review-recovered-lineage-routing.test.ts +0 -47
- package/tests/review-relay-transport-agent.test.ts +119 -46
- package/tests/review-snapshot.test.ts +3 -2
- package/tests/runtime-harness.mjs +362 -196
- package/tests/sdd-agent-tools.test.ts +36 -0
- package/tests/sdd-preflight.test.ts +81 -15
- package/tests/sdd-status.test.ts +109 -110
- package/tests/skill-collision-prefixes.test.ts +5 -8
- package/tests/writer-edit-surface-scope.test.ts +366 -0
- package/themes/Gentleman-Cute.json +94 -0
- package/themes/Gentleman-Sexy.json +92 -0
- package/lib/git-commit-transaction.ts +0 -861
- package/runtime/git-commit-transaction.mjs +0 -862
- package/scripts/run-git-commit-transaction.mjs +0 -35
- package/tests/fixtures/native-review-cli/v2.1.3/sdd-status-engram.json +0 -139
- package/tests/fixtures/native-review-cli/v2.1.3/sdd-status.json +0 -200
- package/tests/git-commit-transaction.test.ts +0 -530
- /package/contracts/review-provider-contract-mirror/{v1.1.0 → v1.2.0}/bundle/schemas/lens.schema.json +0 -0
- /package/contracts/review-provider-contract-mirror/{v1.1.0 → v1.2.0}/bundle/schemas/refuter.schema.json +0 -0
- /package/contracts/review-provider-contract-mirror/{v1.1.0 → v1.2.0}/bundle/vectors/lens.json +0 -0
- /package/contracts/review-provider-contract-mirror/{v1.1.0 → v1.2.0}/bundle/vectors/refuter.json +0 -0
- /package/contracts/review-provider-contract-mirror/{v1.1.0 → v1.2.0}/bundle/vectors/targeted-validator.json +0 -0
- /package/{contracts/review-provider-contract-mirror/v1.1.0/bundle → tests/fixtures/provider-contract-bundle/v1.2.0}/schemas/targeted-validator.schema.json +0 -0
|
@@ -67,6 +67,7 @@ const requiredToolsByAgent: Record<string, string[]> = {
|
|
|
67
67
|
"sdd-init.md": ["read", "grep", "find", "edit", "write", "bash", "mem_search", "mem_get_observation", "mem_save", "mem_update"],
|
|
68
68
|
"sdd-onboard.md": ["read", "grep", "find", "edit", "write", "bash", "mem_search", "mem_get_observation", "mem_save", "mem_update"],
|
|
69
69
|
"sdd-proposal.md": ["read", "grep", "find", "edit", "write", "mem_search", "mem_get_observation", "mem_save"],
|
|
70
|
+
"sdd-research.md": ["read", "grep", "find", "edit", "write", "mem_search", "mem_get_observation", "mem_save"],
|
|
70
71
|
"sdd-spec.md": ["read", "grep", "find", "edit", "write", "mem_search", "mem_get_observation", "mem_save"],
|
|
71
72
|
"sdd-status.md": ["read", "grep", "find", "bash", "mem_search", "mem_get_observation"],
|
|
72
73
|
"sdd-sync.md": ["read", "grep", "find", "edit", "write", "bash", "mem_search", "mem_get_observation", "mem_save", "mem_update"],
|
|
@@ -122,6 +123,41 @@ test("generic non-SDD agents declare exact role tool allowlists", () => {
|
|
|
122
123
|
}
|
|
123
124
|
});
|
|
124
125
|
|
|
126
|
+
test("sdd-verify phase text carries the verify-result envelope and validate-before-persist rule", () => {
|
|
127
|
+
// gentle-pi#535 row 5: the phase must produce a natively admissible report
|
|
128
|
+
// on its first persistence attempt without hunting the format elsewhere.
|
|
129
|
+
const envelopeFields = [
|
|
130
|
+
"schema: gentle-ai.verify-result/v1",
|
|
131
|
+
"evidence_revision: sha256:",
|
|
132
|
+
"verdict:",
|
|
133
|
+
"blockers:",
|
|
134
|
+
"critical_findings:",
|
|
135
|
+
"requirements:",
|
|
136
|
+
"scenarios:",
|
|
137
|
+
"test_command:",
|
|
138
|
+
"test_exit_code:",
|
|
139
|
+
"test_output_hash: sha256:",
|
|
140
|
+
"build_command:",
|
|
141
|
+
"build_exit_code:",
|
|
142
|
+
"build_output_hash: sha256:",
|
|
143
|
+
];
|
|
144
|
+
|
|
145
|
+
const agentSource = readFileSync(join(assetsAgentsDir, "sdd-verify.md"), "utf8");
|
|
146
|
+
assert.match(agentSource, /```yaml\nschema: gentle-ai\.verify-result\/v1\n/);
|
|
147
|
+
for (const field of envelopeFields) {
|
|
148
|
+
assert.ok(agentSource.includes(field), `sdd-verify.md envelope must carry \`${field}\``);
|
|
149
|
+
}
|
|
150
|
+
assert.match(agentSource, /first non-empty content/);
|
|
151
|
+
assert.match(
|
|
152
|
+
agentSource,
|
|
153
|
+
/gentle-ai sdd-verify-validate --input <path\|-> --requirements <n> --scenarios <n>/,
|
|
154
|
+
);
|
|
155
|
+
|
|
156
|
+
const chainSource = readFileSync(join(repoRoot, "assets", "chains", "sdd-verify.chain.md"), "utf8");
|
|
157
|
+
assert.match(chainSource, /gentle-ai\.verify-result\/v1/);
|
|
158
|
+
assert.match(chainSource, /sdd-verify-validate/);
|
|
159
|
+
});
|
|
160
|
+
|
|
125
161
|
test("the retired Pi adversarial role agents are not packaged", () => {
|
|
126
162
|
// gentle-pi#311 P5: the refuter and targeted validator verdicts execute
|
|
127
163
|
// through Go-owned pi processes via provider-rendered self-contained
|
|
@@ -13,7 +13,10 @@ import { tmpdir } from "node:os";
|
|
|
13
13
|
import { join } from "node:path";
|
|
14
14
|
import test from "node:test";
|
|
15
15
|
import {
|
|
16
|
+
collectSddPreflightPreferences,
|
|
17
|
+
DEFAULT_SDD_PREFLIGHT,
|
|
16
18
|
installSddAssets,
|
|
19
|
+
isSddPreflightTrigger,
|
|
17
20
|
readSddPreflightFromDisk,
|
|
18
21
|
sddPreflightDiskPath,
|
|
19
22
|
writeSddPreflightToDisk,
|
|
@@ -27,12 +30,44 @@ async function workspace(): Promise<string> {
|
|
|
27
30
|
const SAMPLE_PREFS: SddPreflightPreferences = {
|
|
28
31
|
executionMode: "auto",
|
|
29
32
|
artifactStore: "engram",
|
|
30
|
-
chainedPrStrategy: "auto-
|
|
33
|
+
chainedPrStrategy: "auto-chain",
|
|
31
34
|
reviewBudgetLines: 400,
|
|
32
35
|
engramAvailable: true,
|
|
33
36
|
prompted: true,
|
|
34
37
|
};
|
|
35
38
|
|
|
39
|
+
function preflightContext(cwd: string, hasUI: boolean, calls: string[] = [], answers: Record<string, string> = {}) {
|
|
40
|
+
return { cwd, hasUI, ui: { select: async (title: string) => (calls.push(`select:${title}`), answers[title]), input: async (title: string) => (calls.push(`input:${title}`), answers[title]), notify: () => {} } } as Parameters<typeof collectSddPreflightPreferences>[0];
|
|
41
|
+
}
|
|
42
|
+
function writeRawPreflight(cwd: string, chainedPrStrategy: string, prompted = true): string {
|
|
43
|
+
const path = sddPreflightDiskPath(cwd); mkdirSync(join(cwd, ".pi", "gentle-ai"), { recursive: true }); writeFileSync(path, JSON.stringify({ executionMode: "auto", artifactStore: "openspec", chainedPrStrategy, reviewBudgetLines: 400, engramAvailable: false, prompted })); return path;
|
|
44
|
+
}
|
|
45
|
+
test("production callers distinguish automatic defaults from explicit preflight prompts", () => {
|
|
46
|
+
const root = join(import.meta.dirname, ".."), gentleAi = readFileSync(join(root, "extensions", "gentle-ai.ts"), "utf8"), sddInit = readFileSync(join(root, "extensions", "sdd-init.ts"), "utf8");
|
|
47
|
+
assert.match(gentleAi, /function runSddPreflight\(\s*ctx: ExtensionContext,\s*promptFields: readonly SddPreflightField\[\] = \[\]\s*\)/s); assert.match(gentleAi, /if \(isSddAgent && !getSddPreflightPreferences\(ctx\)\) \{\s*await runSddPreflight\(ctx\);/s); assert.match(gentleAi, /applyModelConfig: async \(\) => applySavedModelConfig\(ctx\)\s*\},\s*\{\s*promptFields\s*\}\s*\);/s); assert.match(gentleAi, /handler: async \(_args, ctx\) => \{\s*await runSddPreflight\(ctx, SDD_PREFLIGHT_FIELDS\);/s); assert.match(sddInit, /applyModelConfig: \(\) => applySavedModelConfig\(ctx\)\s*\},\s*\{\s*promptFields: \[\]\s*\}\s*\);/s);
|
|
48
|
+
});
|
|
49
|
+
test("capability-constrained artifact selector elision", async () => {
|
|
50
|
+
const calls: string[] = [], prefs = await collectSddPreflightPreferences(preflightContext(await workspace(), true, calls), false, { promptFields: ["artifactStore"] });
|
|
51
|
+
assert.deepEqual(prefs, DEFAULT_SDD_PREFLIGHT); assert.deepEqual(calls, []);
|
|
52
|
+
});
|
|
53
|
+
test("headless/UI canonical-domain parity", async () => {
|
|
54
|
+
const calls: string[] = [], ui = await collectSddPreflightPreferences(preflightContext(await workspace(), true, calls), false), headless = await collectSddPreflightPreferences(preflightContext(await workspace(), false), false);
|
|
55
|
+
assert.deepEqual(ui, DEFAULT_SDD_PREFLIGHT); assert.deepEqual(headless, ui); assert.deepEqual(calls, []);
|
|
56
|
+
});
|
|
57
|
+
test("explicit UI selections override defaults when a field is genuinely unresolved", async () => {
|
|
58
|
+
const calls: string[] = [], prefs = await collectSddPreflightPreferences(preflightContext(await workspace(), true, calls, { "SDD execution mode": "interactive", "SDD artifact store": "engram", "SDD delivery strategy": "auto-chain", "SDD review budget lines": "700" }), true, { persisted: DEFAULT_SDD_PREFLIGHT, promptFields: ["executionMode", "artifactStore", "chainedPrStrategy", "reviewBudgetLines"] });
|
|
59
|
+
assert.deepEqual({ executionMode: prefs.executionMode, artifactStore: prefs.artifactStore, chainedPrStrategy: prefs.chainedPrStrategy, reviewBudgetLines: prefs.reviewBudgetLines }, { executionMode: "interactive", artifactStore: "engram", chainedPrStrategy: "auto-chain", reviewBudgetLines: 700 }); assert.equal(prefs.prompted, true); assert.equal(calls.length, 4);
|
|
60
|
+
});
|
|
61
|
+
test("legacy persisted strategies normalize to canonical values", async () => {
|
|
62
|
+
const mappings = { "auto-forecast": "ask-on-risk", "ask-always": "ask-on-risk", "single-pr-default": "single-pr", "force-chained": "auto-chain" } as const;
|
|
63
|
+
for (const [legacy, canonical] of Object.entries(mappings)) { const cwd = await workspace(); writeRawPreflight(cwd, legacy); assert.equal(readSddPreflightFromDisk(cwd)?.chainedPrStrategy, canonical); }
|
|
64
|
+
});
|
|
65
|
+
test("exception-ok requires narrow delivery-gate provenance", async () => {
|
|
66
|
+
for (const prompted of [false, true]) { const cwd = await workspace(); writeRawPreflight(cwd, "exception-ok", prompted); assert.equal(readSddPreflightFromDisk(cwd)?.chainedPrStrategy, "ask-on-risk"); }
|
|
67
|
+
const accepted = await collectSddPreflightPreferences(preflightContext(await workspace(), false), false, { persisted: { ...DEFAULT_SDD_PREFLIGHT, chainedPrStrategy: "exception-ok" }, acceptSizeException: true }); assert.equal(accepted.chainedPrStrategy, "exception-ok"); assert.equal(accepted.sizeExceptionAccepted, true);
|
|
68
|
+
const durable = await workspace(); writeSddPreflightToDisk(durable, accepted); assert.equal(readSddPreflightFromDisk(durable)?.chainedPrStrategy, "ask-on-risk");
|
|
69
|
+
});
|
|
70
|
+
|
|
36
71
|
test("sddPreflightDiskPath returns project-local .pi/gentle-ai/sdd-preflight.json", async () => {
|
|
37
72
|
const cwd = await workspace();
|
|
38
73
|
const path = sddPreflightDiskPath(cwd);
|
|
@@ -62,18 +97,11 @@ test("readSddPreflightFromDisk returns persisted prefs after write", async () =>
|
|
|
62
97
|
assert.deepEqual(loaded, SAMPLE_PREFS);
|
|
63
98
|
});
|
|
64
99
|
|
|
65
|
-
test("persisted
|
|
66
|
-
const cwd = await workspace();
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
// Simulate a fresh process where in-memory Map is empty — only disk store exists
|
|
72
|
-
const loaded = readSddPreflightFromDisk(cwd);
|
|
73
|
-
assert.ok(loaded !== undefined);
|
|
74
|
-
assert.equal(loaded.artifactStore, "engram");
|
|
75
|
-
assert.equal(loaded.executionMode, "auto");
|
|
76
|
-
assert.equal(loaded.prompted, true);
|
|
100
|
+
test("persisted preferences are reused with zero prompts", async () => {
|
|
101
|
+
const cwd = await workspace(); writeSddPreflightToDisk(cwd, SAMPLE_PREFS);
|
|
102
|
+
const loaded = readSddPreflightFromDisk(cwd); assert.ok(loaded);
|
|
103
|
+
const calls: string[] = [], reused = await collectSddPreflightPreferences(preflightContext(cwd, true, calls), loaded!.engramAvailable, { persisted: loaded });
|
|
104
|
+
assert.deepEqual(reused, loaded); assert.deepEqual(calls, []);
|
|
77
105
|
});
|
|
78
106
|
|
|
79
107
|
test("readSddPreflightFromDisk returns undefined for corrupt JSON", async () => {
|
|
@@ -95,7 +123,7 @@ test("readSddPreflightFromDisk returns undefined for JSON with invalid fields",
|
|
|
95
123
|
assert.equal(readSddPreflightFromDisk(cwd), undefined);
|
|
96
124
|
});
|
|
97
125
|
|
|
98
|
-
test("readSddPreflightFromDisk normalizes unknown chainedPrStrategy to
|
|
126
|
+
test("readSddPreflightFromDisk normalizes unknown chainedPrStrategy to ask-on-risk", async () => {
|
|
99
127
|
const cwd = await workspace();
|
|
100
128
|
const path = sddPreflightDiskPath(cwd);
|
|
101
129
|
mkdirSync(join(cwd, ".pi", "gentle-ai"), { recursive: true });
|
|
@@ -110,7 +138,7 @@ test("readSddPreflightFromDisk normalizes unknown chainedPrStrategy to auto-fore
|
|
|
110
138
|
|
|
111
139
|
const loaded = readSddPreflightFromDisk(cwd);
|
|
112
140
|
assert.ok(loaded !== undefined);
|
|
113
|
-
assert.equal(loaded.chainedPrStrategy, "
|
|
141
|
+
assert.equal(loaded.chainedPrStrategy, "ask-on-risk");
|
|
114
142
|
});
|
|
115
143
|
|
|
116
144
|
test("writeSddPreflightToDisk is non-fatal when directory is not writable (no throw)", async () => {
|
|
@@ -224,3 +252,41 @@ test("forced asset refresh migrates only untouched v0.14 package contracts and p
|
|
|
224
252
|
rmSync(temporaryAgentHome, { recursive: true, force: true });
|
|
225
253
|
}
|
|
226
254
|
});
|
|
255
|
+
|
|
256
|
+
// gentle-ai calls the dual-store mode "hybrid"; this repo called the same
|
|
257
|
+
// operator-facing choice "both". Two names for one concept is how a caller ends
|
|
258
|
+
// up mapping between them by hand. gentle-ai owns the contract, so "hybrid" is
|
|
259
|
+
// canonical here too — but "both" is already persisted in operator preflight
|
|
260
|
+
// files on disk, so it must keep loading rather than fall back to the default.
|
|
261
|
+
test("a persisted legacy 'both' artifact store loads as hybrid", async () => {
|
|
262
|
+
const cwd = await mkdtemp(join(tmpdir(), "sdd-preflight-legacy-"));
|
|
263
|
+
mkdirSync(join(cwd, ".pi", "gentle-ai"), { recursive: true });
|
|
264
|
+
writeFileSync(
|
|
265
|
+
sddPreflightDiskPath(cwd),
|
|
266
|
+
JSON.stringify({ executionMode: "auto", artifactStore: "hybrid", chainedPrStrategy: "ask-on-risk", reviewBudgetLines: 400, engramAvailable: true, prompted: true }),
|
|
267
|
+
);
|
|
268
|
+
|
|
269
|
+
const loaded = readSddPreflightFromDisk(cwd, true);
|
|
270
|
+
assert.equal(loaded?.artifactStore, "hybrid", "legacy 'both' must normalize to the canonical name, not be discarded");
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
test("a persisted canonical 'hybrid' artifact store loads unchanged", async () => {
|
|
274
|
+
const cwd = await mkdtemp(join(tmpdir(), "sdd-preflight-hybrid-"));
|
|
275
|
+
mkdirSync(join(cwd, ".pi", "gentle-ai"), { recursive: true });
|
|
276
|
+
writeFileSync(
|
|
277
|
+
sddPreflightDiskPath(cwd),
|
|
278
|
+
JSON.stringify({ executionMode: "auto", artifactStore: "hybrid", chainedPrStrategy: "ask-on-risk", reviewBudgetLines: 400, engramAvailable: true, prompted: true }),
|
|
279
|
+
);
|
|
280
|
+
|
|
281
|
+
const loaded = readSddPreflightFromDisk(cwd, true);
|
|
282
|
+
assert.equal(loaded?.artifactStore, "hybrid");
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
test("slash SDD preflight trigger accepts the gentle-sdd command prefix", () => {
|
|
286
|
+
for (const text of ["/gentle-sdd-init", "/gentle-sdd-continue", "/gentle-sdd-status fix-rose --json", "/sdd", "/sdd:plan", "/sdd-plan this change"]) {
|
|
287
|
+
assert.equal(isSddPreflightTrigger(text), true, text);
|
|
288
|
+
}
|
|
289
|
+
for (const text of ["/gentle-sddx", "/gentle:sdd-preflight", "/gentle-status", "gentle-sdd-init"]) {
|
|
290
|
+
assert.equal(isSddPreflightTrigger(text), false, text);
|
|
291
|
+
}
|
|
292
|
+
});
|
package/tests/sdd-status.test.ts
CHANGED
|
@@ -92,69 +92,24 @@ test("resolveSddStatus selects the only active change and counts task progress",
|
|
|
92
92
|
assert.match(status.instructions?.apply.join("\n") ?? "", /persisted task checkboxes/);
|
|
93
93
|
});
|
|
94
94
|
|
|
95
|
-
test("resolveSddStatus
|
|
95
|
+
test("resolveSddStatus routes completed implementation through archive without RDD authority", async () => {
|
|
96
96
|
const cwd = await workspace();
|
|
97
97
|
const root = seedChange(cwd);
|
|
98
98
|
write(join(root, "tasks.md"), "# Tasks\n\n- [x] 1.1 Done\n");
|
|
99
99
|
write(join(root, "verify-report.md"), "# Verify\n\nPASS\n");
|
|
100
100
|
write(join(root, "sync-report.md"), "# Sync\n\nPASS\n");
|
|
101
|
-
let resolverCalls = 0;
|
|
102
|
-
const planning = resolveSddStatus({
|
|
103
|
-
cwd,
|
|
104
|
-
changeName: "add-auth",
|
|
105
|
-
reviewAuthority: {
|
|
106
|
-
expected: false,
|
|
107
|
-
resolve: () => {
|
|
108
|
-
resolverCalls += 1;
|
|
109
|
-
return { activeAuthorityId: "must-not-run" };
|
|
110
|
-
},
|
|
111
|
-
},
|
|
112
|
-
});
|
|
113
|
-
assert.equal(resolverCalls, 0);
|
|
114
|
-
assert.equal(planning.blockedReasons.some((reason) => reason.startsWith("resolve-review:")), false);
|
|
115
|
-
|
|
116
|
-
const invalid = resolveSddStatus({
|
|
117
|
-
cwd,
|
|
118
|
-
changeName: "add-auth",
|
|
119
|
-
reviewAuthority: {
|
|
120
|
-
expected: true,
|
|
121
|
-
resolve: () => {
|
|
122
|
-
throw new Error("stale compact successor");
|
|
123
|
-
},
|
|
124
|
-
},
|
|
125
|
-
});
|
|
126
|
-
assert.equal(invalid.applyState, "blocked");
|
|
127
|
-
assert.equal(invalid.dependencies.archive, "blocked");
|
|
128
|
-
assert.equal(invalid.nextRecommended, "resolve-review");
|
|
129
|
-
assert.match(invalid.blockedReasons.join("\n"), /^resolve-review: stale compact successor/m);
|
|
130
101
|
|
|
131
|
-
const
|
|
132
|
-
cwd,
|
|
133
|
-
changeName: "add-auth",
|
|
134
|
-
reviewAuthority: {
|
|
135
|
-
expected: true,
|
|
136
|
-
resolve: () => ({ activeAuthorityId: "validated-compact-authority" }),
|
|
137
|
-
},
|
|
138
|
-
});
|
|
139
|
-
assert.equal(valid.applyState, "all_done");
|
|
140
|
-
assert.equal(valid.dependencies.archive, "ready");
|
|
141
|
-
assert.equal(valid.blockedReasons.some((reason) => reason.startsWith("resolve-review:")), false);
|
|
142
|
-
});
|
|
102
|
+
const status = resolveSddStatus({ cwd, changeName: "add-auth" });
|
|
143
103
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
nativeReviewReadiness: { expected: true, ready: false, reason: "binding is missing" },
|
|
151
|
-
});
|
|
152
|
-
assert.equal(status.applyState, "blocked");
|
|
153
|
-
assert.equal(status.nextRecommended, "resolve-review");
|
|
154
|
-
assert.deepEqual(status.blockedReasons, ["resolve-review: binding is missing"]);
|
|
104
|
+
assert.equal(status.applyState, "all_done");
|
|
105
|
+
assert.equal(status.dependencies.verify, "all_done");
|
|
106
|
+
assert.equal(status.dependencies.sync, "all_done");
|
|
107
|
+
assert.equal(status.dependencies.archive, "ready");
|
|
108
|
+
assert.equal(status.nextRecommended, "sdd-archive");
|
|
109
|
+
assert.equal(status.blockedReasons.some((reason) => reason.startsWith("resolve-review:")), false);
|
|
155
110
|
});
|
|
156
111
|
|
|
157
|
-
test("resolveSddStatus
|
|
112
|
+
test("resolveSddStatus ignores legacy parent review actions after implementation", async () => {
|
|
158
113
|
const cwd = await workspace();
|
|
159
114
|
const root = seedChange(cwd);
|
|
160
115
|
write(
|
|
@@ -174,8 +129,8 @@ test("resolveSddStatus routes completed implementation to parent lifecycle and s
|
|
|
174
129
|
assert.deepEqual(status.taskArtifactErrors, []);
|
|
175
130
|
assert.equal(status.applyState, "all_done");
|
|
176
131
|
assert.equal(status.dependencies.apply, "all_done");
|
|
177
|
-
assert.equal(status.
|
|
178
|
-
assert.
|
|
132
|
+
assert.equal(status.dependencies.verify, "ready");
|
|
133
|
+
assert.equal(status.nextRecommended, "sdd-verify");
|
|
179
134
|
});
|
|
180
135
|
|
|
181
136
|
test("resolveSddStatus treats malformed ownership as unresolved implementation work", async () => {
|
|
@@ -197,7 +152,7 @@ test("resolveSddStatus treats malformed ownership as unresolved implementation w
|
|
|
197
152
|
assert.match(status.blockedReasons.join("\n"), /task ownership marker/i);
|
|
198
153
|
});
|
|
199
154
|
|
|
200
|
-
test("resolveSddStatus routes completed legacy implementation to
|
|
155
|
+
test("resolveSddStatus routes completed legacy implementation directly to verify", async () => {
|
|
201
156
|
const cwd = await workspace();
|
|
202
157
|
const root = seedChange(cwd);
|
|
203
158
|
write(join(root, "tasks.md"), "# Tasks\n\n- [x] 1.1 Build foundation\n");
|
|
@@ -206,7 +161,8 @@ test("resolveSddStatus routes completed legacy implementation to parent lifecycl
|
|
|
206
161
|
|
|
207
162
|
assert.equal(status.applyState, "all_done");
|
|
208
163
|
assert.equal(status.dependencies.apply, "all_done");
|
|
209
|
-
assert.equal(status.
|
|
164
|
+
assert.equal(status.dependencies.verify, "ready");
|
|
165
|
+
assert.equal(status.nextRecommended, "sdd-verify");
|
|
210
166
|
});
|
|
211
167
|
|
|
212
168
|
test("resolveSddStatus blocks sync when verify report is not clearly passing", async () => {
|
|
@@ -215,11 +171,7 @@ test("resolveSddStatus blocks sync when verify report is not clearly passing", a
|
|
|
215
171
|
write(join(root, "apply-progress.md"), "# Apply\n\nSome work completed.\n");
|
|
216
172
|
write(join(root, "verify-report.md"), "# Verify\n\nTODO: tests not run yet\n");
|
|
217
173
|
|
|
218
|
-
const status = resolveSddStatus({
|
|
219
|
-
cwd,
|
|
220
|
-
changeName: "add-auth",
|
|
221
|
-
reviewAuthority: { expected: true, resolve: () => ({ activeAuthorityId: "approved" }) },
|
|
222
|
-
});
|
|
174
|
+
const status = resolveSddStatus({ cwd, changeName: "add-auth" });
|
|
223
175
|
|
|
224
176
|
assert.equal(status.dependencies.verify, "ready");
|
|
225
177
|
assert.equal(status.dependencies.sync, "blocked");
|
|
@@ -233,11 +185,7 @@ test("resolveSddStatus rejects negated pass and sync-complete phrases", async ()
|
|
|
233
185
|
write(join(root, "verify-report.md"), "# Verify\n\nStatus: not passed\n");
|
|
234
186
|
write(join(root, "sync-report.md"), "# Sync\n\nSync complete: no\n");
|
|
235
187
|
|
|
236
|
-
const status = resolveSddStatus({
|
|
237
|
-
cwd,
|
|
238
|
-
changeName: "add-auth",
|
|
239
|
-
reviewAuthority: { expected: true, resolve: () => ({ activeAuthorityId: "approved" }) },
|
|
240
|
-
});
|
|
188
|
+
const status = resolveSddStatus({ cwd, changeName: "add-auth" });
|
|
241
189
|
|
|
242
190
|
assert.equal(status.dependencies.verify, "ready");
|
|
243
191
|
assert.equal(status.dependencies.sync, "blocked");
|
|
@@ -334,11 +282,7 @@ test("resolveSddStatus blocks stale sync report when current verify is not passi
|
|
|
334
282
|
write(join(root, "verify-report.md"), "# Verify\n\nStatus: not passed\n");
|
|
335
283
|
write(join(root, "sync-report.md"), "# Sync\n\nPASS\n");
|
|
336
284
|
|
|
337
|
-
const status = resolveSddStatus({
|
|
338
|
-
cwd,
|
|
339
|
-
changeName: "add-auth",
|
|
340
|
-
reviewAuthority: { expected: true, resolve: () => ({ activeAuthorityId: "approved" }) },
|
|
341
|
-
});
|
|
285
|
+
const status = resolveSddStatus({ cwd, changeName: "add-auth" });
|
|
342
286
|
|
|
343
287
|
assert.equal(status.dependencies.verify, "ready");
|
|
344
288
|
assert.equal(status.dependencies.sync, "blocked");
|
|
@@ -381,11 +325,7 @@ test("resolveSddStatus marks archive ready only after clean verify, sync, and co
|
|
|
381
325
|
write(join(root, "verify-report.md"), "# Verify\n\nPASS\n");
|
|
382
326
|
write(join(root, "sync-report.md"), "# Sync\n\nPASS\n");
|
|
383
327
|
|
|
384
|
-
const status = resolveSddStatus({
|
|
385
|
-
cwd,
|
|
386
|
-
changeName: "add-auth",
|
|
387
|
-
reviewAuthority: { expected: true, resolve: () => ({ activeAuthorityId: "approved" }) },
|
|
388
|
-
});
|
|
328
|
+
const status = resolveSddStatus({ cwd, changeName: "add-auth" });
|
|
389
329
|
|
|
390
330
|
assert.equal(status.dependencies.archive, "ready");
|
|
391
331
|
assert.equal(status.nextRecommended, "sdd-archive");
|
|
@@ -428,13 +368,13 @@ test("resolveSddStatus with artifactStore none returns non-authoritative status
|
|
|
428
368
|
assert.equal(status.nextRecommended, "resolve-via-engram");
|
|
429
369
|
});
|
|
430
370
|
|
|
431
|
-
test("resolveSddStatus with artifactStore
|
|
371
|
+
test("resolveSddStatus with artifactStore hybrid uses disk scan and reflects store", async () => {
|
|
432
372
|
const cwd = await workspace();
|
|
433
373
|
seedChange(cwd);
|
|
434
374
|
|
|
435
|
-
const status = resolveSddStatus({ cwd, artifactStore: "
|
|
375
|
+
const status = resolveSddStatus({ cwd, artifactStore: "hybrid", changeName: "add-auth" });
|
|
436
376
|
|
|
437
|
-
assert.equal(status.artifactStore, "
|
|
377
|
+
assert.equal(status.artifactStore, "hybrid");
|
|
438
378
|
assert.equal(status.changeName, "add-auth");
|
|
439
379
|
assert.notEqual(status.nextRecommended, "resolve-via-engram");
|
|
440
380
|
});
|
|
@@ -484,13 +424,13 @@ test("parseSddStatusCommandArgs extracts change and json flag", () => {
|
|
|
484
424
|
});
|
|
485
425
|
});
|
|
486
426
|
|
|
487
|
-
test("resolveSddStatus with artifactStore
|
|
427
|
+
test("resolveSddStatus with artifactStore hybrid and NO openspec dir returns non-authoritative status", async () => {
|
|
488
428
|
const cwd = await workspace();
|
|
489
|
-
// No openspec directory —
|
|
429
|
+
// No openspec directory — the hybrid store without disk backing is non-authoritative
|
|
490
430
|
|
|
491
|
-
const status = resolveSddStatus({ cwd, artifactStore: "
|
|
431
|
+
const status = resolveSddStatus({ cwd, artifactStore: "hybrid", changeName: "my-change" });
|
|
492
432
|
|
|
493
|
-
assert.equal(status.artifactStore, "
|
|
433
|
+
assert.equal(status.artifactStore, "hybrid");
|
|
494
434
|
assert.equal(status.changeName, "my-change");
|
|
495
435
|
assert.deepEqual(status.blockedReasons, []);
|
|
496
436
|
assert.equal(status.nextRecommended, "resolve-via-engram");
|
|
@@ -499,13 +439,13 @@ test("resolveSddStatus with artifactStore both and NO openspec dir returns non-a
|
|
|
499
439
|
assert.equal(status.dependencies.archive, "not_applicable");
|
|
500
440
|
});
|
|
501
441
|
|
|
502
|
-
test("resolveSddStatus with artifactStore
|
|
442
|
+
test("resolveSddStatus with artifactStore hybrid and existing openspec dir runs authoritative disk scan", async () => {
|
|
503
443
|
const cwd = await workspace();
|
|
504
444
|
seedChange(cwd);
|
|
505
445
|
|
|
506
|
-
const status = resolveSddStatus({ cwd, artifactStore: "
|
|
446
|
+
const status = resolveSddStatus({ cwd, artifactStore: "hybrid", changeName: "add-auth" });
|
|
507
447
|
|
|
508
|
-
assert.equal(status.artifactStore, "
|
|
448
|
+
assert.equal(status.artifactStore, "hybrid");
|
|
509
449
|
assert.equal(status.changeName, "add-auth");
|
|
510
450
|
assert.notEqual(status.nextRecommended, "resolve-via-engram");
|
|
511
451
|
assert.equal(status.artifacts.proposal, "done");
|
|
@@ -526,13 +466,13 @@ test("isNonAuthoritativeStatus reads typed isNonAuthoritative field and matches
|
|
|
526
466
|
assert.equal(isNonAuthoritativeStatus(none), true);
|
|
527
467
|
assert.equal(none.nextRecommended, "resolve-via-engram");
|
|
528
468
|
|
|
529
|
-
const bothWithoutOpenspec = resolveSddStatus({ cwd, artifactStore: "
|
|
469
|
+
const bothWithoutOpenspec = resolveSddStatus({ cwd, artifactStore: "hybrid", changeName: "x" });
|
|
530
470
|
assert.equal(bothWithoutOpenspec.isNonAuthoritative, true);
|
|
531
471
|
assert.equal(isNonAuthoritativeStatus(bothWithoutOpenspec), true);
|
|
532
472
|
assert.equal(bothWithoutOpenspec.nextRecommended, "resolve-via-engram");
|
|
533
473
|
|
|
534
474
|
seedChange(cwd);
|
|
535
|
-
const bothWithOpenspec = resolveSddStatus({ cwd, artifactStore: "
|
|
475
|
+
const bothWithOpenspec = resolveSddStatus({ cwd, artifactStore: "hybrid", changeName: "add-auth" });
|
|
536
476
|
assert.equal(bothWithOpenspec.isNonAuthoritative, false);
|
|
537
477
|
assert.equal(isNonAuthoritativeStatus(bothWithOpenspec), false);
|
|
538
478
|
assert.notEqual(bothWithOpenspec.nextRecommended, "resolve-via-engram");
|
|
@@ -551,12 +491,12 @@ test("isNonAuthoritative boolean field is set correctly across all store/disk co
|
|
|
551
491
|
assert.equal(none.isNonAuthoritative, true);
|
|
552
492
|
|
|
553
493
|
// both without openspec/ → non-authoritative
|
|
554
|
-
const bothWithout = resolveSddStatus({ cwd, artifactStore: "
|
|
494
|
+
const bothWithout = resolveSddStatus({ cwd, artifactStore: "hybrid", changeName: "x" });
|
|
555
495
|
assert.equal(bothWithout.isNonAuthoritative, true);
|
|
556
496
|
|
|
557
497
|
// both WITH openspec/ and seeded change → authoritative
|
|
558
498
|
seedChange(cwd);
|
|
559
|
-
const bothWith = resolveSddStatus({ cwd, artifactStore: "
|
|
499
|
+
const bothWith = resolveSddStatus({ cwd, artifactStore: "hybrid", changeName: "add-auth" });
|
|
560
500
|
assert.equal(bothWith.isNonAuthoritative, false);
|
|
561
501
|
|
|
562
502
|
// openspec (default disk scan, seeded) → authoritative
|
|
@@ -565,30 +505,30 @@ test("isNonAuthoritative boolean field is set correctly across all store/disk co
|
|
|
565
505
|
});
|
|
566
506
|
|
|
567
507
|
// Fix 4 item 1 — both + openspec/ dir present + change NOT on disk → non-authoritative
|
|
568
|
-
test("resolveSddStatus with artifactStore
|
|
508
|
+
test("resolveSddStatus with artifactStore hybrid, openspec dir present but change not on disk returns non-authoritative", async () => {
|
|
569
509
|
const cwd = await workspace();
|
|
570
510
|
// Create an openspec/changes dir with a different change — not the requested one
|
|
571
511
|
mkdirSync(join(cwd, "openspec", "changes", "other-change"), { recursive: true });
|
|
572
512
|
|
|
573
|
-
const status = resolveSddStatus({ cwd, artifactStore: "
|
|
513
|
+
const status = resolveSddStatus({ cwd, artifactStore: "hybrid", changeName: "missing-change" });
|
|
574
514
|
|
|
575
515
|
assert.equal(status.isNonAuthoritative, true);
|
|
576
516
|
assert.equal(status.nextRecommended, "resolve-via-engram");
|
|
577
517
|
assert.deepEqual(status.blockedReasons, []);
|
|
578
518
|
assert.equal(status.applyState, "not_applicable");
|
|
579
|
-
assert.equal(status.artifactStore, "
|
|
519
|
+
assert.equal(status.artifactStore, "hybrid");
|
|
580
520
|
// Must NOT be treated as blocked
|
|
581
521
|
assert.notEqual(status.applyState, "blocked");
|
|
582
522
|
});
|
|
583
523
|
|
|
584
524
|
// Fix 4 item 2 — strengthen existing both-with-openspec-and-seeded-change test
|
|
585
|
-
test("resolveSddStatus with artifactStore
|
|
525
|
+
test("resolveSddStatus with artifactStore hybrid, openspec dir present and change on disk is authoritative", async () => {
|
|
586
526
|
const cwd = await workspace();
|
|
587
527
|
seedChange(cwd);
|
|
588
528
|
|
|
589
|
-
const status = resolveSddStatus({ cwd, artifactStore: "
|
|
529
|
+
const status = resolveSddStatus({ cwd, artifactStore: "hybrid", changeName: "add-auth" });
|
|
590
530
|
|
|
591
|
-
assert.equal(status.artifactStore, "
|
|
531
|
+
assert.equal(status.artifactStore, "hybrid");
|
|
592
532
|
assert.equal(status.changeName, "add-auth");
|
|
593
533
|
// Must be authoritative
|
|
594
534
|
assert.equal(isNonAuthoritativeStatus(status), false);
|
|
@@ -618,7 +558,7 @@ test("renderSddDispatcherMarkdown for both-without-openspec does NOT render Read
|
|
|
618
558
|
const cwd = await workspace();
|
|
619
559
|
// No openspec directory — both store is non-authoritative
|
|
620
560
|
|
|
621
|
-
const status = resolveSddStatus({ cwd, artifactStore: "
|
|
561
|
+
const status = resolveSddStatus({ cwd, artifactStore: "hybrid", changeName: "fix-x" });
|
|
622
562
|
const markdown = renderSddDispatcherMarkdown(status);
|
|
623
563
|
|
|
624
564
|
assert.doesNotMatch(markdown, /### Ready/);
|
|
@@ -628,7 +568,7 @@ test("renderSddDispatcherMarkdown for both-without-openspec does NOT render Read
|
|
|
628
568
|
test("renderNativeSddPhasePrompt for both-without-openspec emits non-authoritative line", async () => {
|
|
629
569
|
const cwd = await workspace();
|
|
630
570
|
|
|
631
|
-
const status = resolveSddStatus({ cwd, artifactStore: "
|
|
571
|
+
const status = resolveSddStatus({ cwd, artifactStore: "hybrid", changeName: "fix-x" });
|
|
632
572
|
const prompt = renderNativeSddPhasePrompt(status, "apply");
|
|
633
573
|
|
|
634
574
|
assert.match(prompt, /non-authoritative/);
|
|
@@ -646,17 +586,17 @@ test("renderPhaseInstructions for not_applicable applyState emits neutral line",
|
|
|
646
586
|
});
|
|
647
587
|
|
|
648
588
|
// Fix 4 item 1 — both + openspec/ + ZERO changes + no changeName → non-authoritative
|
|
649
|
-
test("resolveSddStatus
|
|
589
|
+
test("resolveSddStatus hybrid + openspec/ dir + zero active changes + no changeName returns non-authoritative", async () => {
|
|
650
590
|
const cwd = await workspace();
|
|
651
591
|
// openspec/ dir exists but holds no active changes (only the changes/ subdir)
|
|
652
592
|
mkdirSync(join(cwd, "openspec", "changes"), { recursive: true });
|
|
653
593
|
|
|
654
|
-
const status = resolveSddStatus({ cwd, artifactStore: "
|
|
594
|
+
const status = resolveSddStatus({ cwd, artifactStore: "hybrid" });
|
|
655
595
|
|
|
656
596
|
assert.equal(status.isNonAuthoritative, true);
|
|
657
597
|
assert.equal(status.nextRecommended, "resolve-via-engram");
|
|
658
598
|
assert.deepEqual(status.blockedReasons, []);
|
|
659
|
-
assert.equal(status.artifactStore, "
|
|
599
|
+
assert.equal(status.artifactStore, "hybrid");
|
|
660
600
|
assert.equal(status.applyState, "not_applicable");
|
|
661
601
|
assert.equal(status.dependencies.apply, "not_applicable");
|
|
662
602
|
assert.equal(status.dependencies.archive, "not_applicable");
|
|
@@ -665,12 +605,12 @@ test("resolveSddStatus both + openspec/ dir + zero active changes + no changeNam
|
|
|
665
605
|
});
|
|
666
606
|
|
|
667
607
|
// Fix 4 item 2 — both + openspec/ + MULTIPLE changes + no changeName → authoritative select-change
|
|
668
|
-
test("resolveSddStatus
|
|
608
|
+
test("resolveSddStatus hybrid + openspec/ dir + multiple active changes + no changeName stays authoritative", async () => {
|
|
669
609
|
const cwd = await workspace();
|
|
670
610
|
mkdirSync(join(cwd, "openspec", "changes", "alpha"), { recursive: true });
|
|
671
611
|
mkdirSync(join(cwd, "openspec", "changes", "beta"), { recursive: true });
|
|
672
612
|
|
|
673
|
-
const status = resolveSddStatus({ cwd, artifactStore: "
|
|
613
|
+
const status = resolveSddStatus({ cwd, artifactStore: "hybrid" });
|
|
674
614
|
|
|
675
615
|
// Authoritative ambiguous-selection behavior must be preserved
|
|
676
616
|
assert.equal(status.isNonAuthoritative, false);
|
|
@@ -679,16 +619,16 @@ test("resolveSddStatus both + openspec/ dir + multiple active changes + no chang
|
|
|
679
619
|
});
|
|
680
620
|
|
|
681
621
|
// Fix 4 item 3 — both + openspec/ + ONE resolvable change → authoritative
|
|
682
|
-
test("resolveSddStatus
|
|
622
|
+
test("resolveSddStatus hybrid + openspec/ dir + exactly one active change is authoritative", async () => {
|
|
683
623
|
const cwd = await workspace();
|
|
684
624
|
seedChange(cwd);
|
|
685
625
|
|
|
686
626
|
// No changeName supplied — should auto-select the single change
|
|
687
|
-
const status = resolveSddStatus({ cwd, artifactStore: "
|
|
627
|
+
const status = resolveSddStatus({ cwd, artifactStore: "hybrid" });
|
|
688
628
|
|
|
689
629
|
assert.equal(status.isNonAuthoritative, false);
|
|
690
630
|
assert.equal(status.changeName, "add-auth");
|
|
691
|
-
assert.equal(status.artifactStore, "
|
|
631
|
+
assert.equal(status.artifactStore, "hybrid");
|
|
692
632
|
assert.notEqual(status.applyState, "not_applicable");
|
|
693
633
|
assert.notEqual(status.nextRecommended, "resolve-via-engram");
|
|
694
634
|
assert.equal(status.artifacts.proposal, "done");
|
|
@@ -719,14 +659,73 @@ test("resolveSddStatus openspec + named change missing still blocks", async () =
|
|
|
719
659
|
assert.notEqual(status.nextRecommended, "resolve-via-engram");
|
|
720
660
|
});
|
|
721
661
|
|
|
662
|
+
// Issue #535 — post-archive status exposes explicit completion and the archived artifact location
|
|
663
|
+
test("resolveSddStatus projects archived terminal state when the change lives in the archive", async () => {
|
|
664
|
+
const cwd = await workspace();
|
|
665
|
+
mkdirSync(join(cwd, "openspec", "changes"), { recursive: true });
|
|
666
|
+
write(
|
|
667
|
+
join(cwd, "openspec", "changes", "archive", "2026-01-02-add-auth", "proposal.md"),
|
|
668
|
+
"# Proposal\n",
|
|
669
|
+
);
|
|
670
|
+
|
|
671
|
+
const status = resolveSddStatus({ cwd, changeName: "add-auth" });
|
|
672
|
+
|
|
673
|
+
assert.equal(status.changeName, "add-auth");
|
|
674
|
+
assert.equal(status.nextRecommended, "archived");
|
|
675
|
+
assert.deepEqual(status.archived, {
|
|
676
|
+
path: join("openspec", "changes", "archive", "2026-01-02-add-auth"),
|
|
677
|
+
});
|
|
678
|
+
assert.deepEqual(status.blockedReasons, []);
|
|
679
|
+
assert.equal(status.dependencies.archive, "all_done");
|
|
680
|
+
assert.doesNotMatch(status.nextRecommended, /sdd-new|Start an SDD change|not found/i);
|
|
681
|
+
});
|
|
682
|
+
|
|
683
|
+
test("resolveSddStatus archived projection prefers the newest archive date", async () => {
|
|
684
|
+
const cwd = await workspace();
|
|
685
|
+
mkdirSync(join(cwd, "openspec", "changes"), { recursive: true });
|
|
686
|
+
mkdirSync(join(cwd, "openspec", "changes", "archive", "2026-01-01-add-auth"), { recursive: true });
|
|
687
|
+
mkdirSync(join(cwd, "openspec", "changes", "archive", "2026-03-04-add-auth"), { recursive: true });
|
|
688
|
+
|
|
689
|
+
const status = resolveSddStatus({ cwd, changeName: "add-auth" });
|
|
690
|
+
|
|
691
|
+
assert.equal(status.nextRecommended, "archived");
|
|
692
|
+
assert.deepEqual(status.archived, {
|
|
693
|
+
path: join("openspec", "changes", "archive", "2026-03-04-add-auth"),
|
|
694
|
+
});
|
|
695
|
+
});
|
|
696
|
+
|
|
697
|
+
test("resolveSddStatus still blocks a change that never existed and has no archive entry", async () => {
|
|
698
|
+
const cwd = await workspace();
|
|
699
|
+
mkdirSync(join(cwd, "openspec", "changes"), { recursive: true });
|
|
700
|
+
mkdirSync(join(cwd, "openspec", "changes", "archive", "2026-01-01-unrelated"), { recursive: true });
|
|
701
|
+
|
|
702
|
+
const status = resolveSddStatus({ cwd, changeName: "ghost" });
|
|
703
|
+
|
|
704
|
+
assert.equal(status.archived, undefined);
|
|
705
|
+
assert.match(status.blockedReasons.join("\n"), /Active change not found/);
|
|
706
|
+
assert.notEqual(status.nextRecommended, "archived");
|
|
707
|
+
});
|
|
708
|
+
|
|
709
|
+
test("resolveSddStatus archive matching requires the exact change-name suffix", async () => {
|
|
710
|
+
const cwd = await workspace();
|
|
711
|
+
mkdirSync(join(cwd, "openspec", "changes"), { recursive: true });
|
|
712
|
+
mkdirSync(join(cwd, "openspec", "changes", "archive", "2026-01-01-foo-bar-baz"), { recursive: true });
|
|
713
|
+
|
|
714
|
+
const status = resolveSddStatus({ cwd, changeName: "foo-bar" });
|
|
715
|
+
|
|
716
|
+
assert.equal(status.archived, undefined);
|
|
717
|
+
assert.match(status.blockedReasons.join("\n"), /Active change not found/);
|
|
718
|
+
assert.notEqual(status.nextRecommended, "archived");
|
|
719
|
+
});
|
|
720
|
+
|
|
722
721
|
// Fix 4 render test — non-authoritative both status → dispatcher shows "both" not "Engram or none"
|
|
723
722
|
test("renderSddDispatcherMarkdown for non-authoritative both status shows artifact store 'both'", async () => {
|
|
724
723
|
const cwd = await workspace();
|
|
725
724
|
|
|
726
|
-
const status = resolveSddStatus({ cwd, artifactStore: "
|
|
725
|
+
const status = resolveSddStatus({ cwd, artifactStore: "hybrid", changeName: "fix-x" });
|
|
727
726
|
const markdown = renderSddDispatcherMarkdown(status);
|
|
728
727
|
|
|
729
|
-
assert.match(markdown, /artifact store:
|
|
728
|
+
assert.match(markdown, /artifact store: hybrid/);
|
|
730
729
|
assert.doesNotMatch(markdown, /Engram or none/);
|
|
731
730
|
assert.match(markdown, /resolve via Engram/i);
|
|
732
731
|
});
|
|
@@ -14,21 +14,18 @@ function readSkillName(dir: string): string | undefined {
|
|
|
14
14
|
const PREFIXED_NAMES: Record<string, string> = {
|
|
15
15
|
"branch-pr": "gentle-ai-branch-pr",
|
|
16
16
|
"chained-pr": "gentle-ai-chained-pr",
|
|
17
|
+
"cognitive-doc-design": "gentle-ai-cognitive-doc-design",
|
|
18
|
+
"comment-writer": "gentle-ai-comment-writer",
|
|
17
19
|
"issue-creation": "gentle-ai-issue-creation",
|
|
18
20
|
"judgment-day": "gentle-ai-judgment-day",
|
|
19
21
|
"rdd-defect-workflow": "gentle-ai-rdd-defect-workflow",
|
|
20
22
|
"skill-creator": "gentle-ai-skill-creator",
|
|
21
23
|
"skill-improver": "gentle-ai-skill-improver",
|
|
24
|
+
"skill-registry": "gentle-ai-skill-registry",
|
|
25
|
+
"work-unit-commits": "gentle-ai-work-unit-commits",
|
|
22
26
|
};
|
|
23
27
|
|
|
24
|
-
const UNPREFIXED_DIRS = [
|
|
25
|
-
"skill-registry",
|
|
26
|
-
"release",
|
|
27
|
-
"work-unit-commits",
|
|
28
|
-
"gentle-ai",
|
|
29
|
-
"comment-writer",
|
|
30
|
-
"cognitive-doc-design",
|
|
31
|
-
];
|
|
28
|
+
const UNPREFIXED_DIRS = ["gentle-ai", "release"];
|
|
32
29
|
|
|
33
30
|
for (const [dir, expectedName] of Object.entries(PREFIXED_NAMES)) {
|
|
34
31
|
test(`skills/${dir}/SKILL.md frontmatter name is prefixed`, () => {
|