gentle-pi 2.7.0 → 3.0.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 +24 -6
- package/assets/agents/gentle-ai-worker.md +5 -1
- package/assets/agents/sdd-apply.md +9 -7
- package/assets/agents/sdd-archive.md +42 -23
- package/assets/agents/sdd-proposal.md +2 -2
- package/assets/agents/sdd-remediate.md +4 -4
- package/assets/agents/sdd-research.md +20 -48
- package/assets/agents/sdd-tasks.md +5 -5
- package/assets/agents/sdd-verify.md +6 -28
- package/assets/chains/sdd-full.chain.md +4 -22
- package/assets/chains/sdd-verify.chain.md +3 -12
- package/assets/orchestrator-delegation.md +33 -3
- package/assets/orchestrator-memory.md +20 -7
- package/assets/orchestrator.md +5 -3
- package/assets/sdd-orchestrator-workflow.md +25 -58
- package/assets/support/sdd-status-contract.md +9 -12
- package/docs/gentle-shell.md +14 -4
- package/docs/readme-reference.md +171 -32
- package/extensions/codegraph-tools.ts +2 -0
- package/extensions/gentle-agents.ts +281 -361
- package/extensions/gentle-ai.ts +588 -117
- package/extensions/gentle-shell.ts +74 -30
- package/extensions/pi-pretty.ts +63 -14
- package/extensions/quiet-tools.ts +1 -2
- package/extensions/startup-banner.ts +10 -9
- package/lib/agent-home.ts +8 -0
- package/lib/agent-profile-pin.ts +336 -0
- package/lib/agent-profiles.ts +28 -8
- package/lib/agents-config.ts +24 -2
- package/lib/agents-history.ts +3 -97
- package/lib/agents-keys.ts +27 -0
- package/lib/agents-protocol.ts +2 -15
- package/lib/agents-runner.ts +67 -111
- package/lib/agents-session-transport.ts +691 -0
- package/lib/command-palette-catalog.ts +87 -0
- package/lib/command-palette.ts +346 -0
- package/lib/native-choice-list.ts +5 -0
- package/lib/native-review-cli.ts +10 -97
- package/lib/review-publication-gate.ts +11 -1
- package/lib/review-repository.ts +1 -1
- package/lib/review-snapshot.ts +1 -0
- package/lib/review-transaction.ts +4 -2
- package/lib/sdd-preflight.ts +2 -1
- package/lib/sdd-research-capabilities.ts +18 -152
- package/lib/sdd-status.ts +7 -779
- package/lib/session-change-capture.ts +2 -1
- package/lib/session-changes.ts +8 -1
- package/lib/shell-bar.ts +21 -12
- package/lib/shell-card.ts +8 -12
- package/lib/shell-changes.ts +3 -2
- package/lib/shell-prompt.ts +25 -8
- package/lib/shell-sidebar-banner.ts +2 -2
- package/lib/shell-sidebar-layout.ts +5 -2
- package/lib/windows-session-transport.ts +877 -0
- package/package.json +3 -3
- package/runtime/native-review-cli.mjs +9 -96
- package/runtime/windows-session-transport.ps1 +791 -0
- package/scripts/test-packed-runner.mjs +1668 -20
- package/scripts/verify-package-files.mjs +0 -1
- package/tests/agent-home.test.ts +52 -0
- package/tests/agent-profiles.test.ts +30 -1
- package/tests/agents-config.test.ts +44 -0
- package/tests/agents-history.test.ts +12 -24
- package/tests/agents-runner.test.ts +307 -58
- package/tests/agents-session-transport-process.test.ts +249 -0
- package/tests/agents-session-transport.test.ts +823 -0
- package/tests/artifact-language.test.ts +10 -7
- package/tests/command-palette.test.ts +378 -0
- package/tests/delegated-key-learnings-contract.test.ts +2 -2
- package/tests/fixtures/agents-session-transport-process.mjs +108 -0
- package/tests/fixtures/legacy/sdd-research-v2.5.0.md +54 -0
- package/tests/fixtures/windows-session-bootstrap.ps1 +129 -0
- package/tests/fixtures/windows-session-compile.ps1 +110 -0
- package/tests/gentle-agents.test.ts +849 -356
- package/tests/gentle-ai.test.ts +472 -4
- package/tests/gentle-shell.test.ts +201 -8
- package/tests/native-choice-list.test.ts +13 -0
- package/tests/native-review-cli.test.ts +0 -33
- package/tests/odd-routing-contract.test.ts +208 -0
- package/tests/orchestrator-budget.test.ts +17 -2
- package/tests/package-manifest.test.ts +115 -27
- package/tests/persona-single-channel.test.ts +3 -3
- package/tests/pi-pretty.test.ts +45 -0
- package/tests/profile-pin.test.ts +370 -0
- package/tests/quiet-tool-rendering.test.ts +32 -5
- package/tests/review-contract-prompt.test.ts +9 -0
- package/tests/review-controller.test.ts +0 -44
- package/tests/review-session-standing-permission-ipc.test.ts +427 -13
- package/tests/runtime-harness.mjs +4 -4
- package/tests/sdd-agent-tools.test.ts +15 -36
- package/tests/sdd-archive-replay.test.ts +82 -0
- package/tests/sdd-classical-continuation.test.ts +74 -0
- package/tests/sdd-execution-routing-contract.test.ts +18 -2
- package/tests/sdd-managed-runtime-settlement.test.ts +42 -330
- package/tests/sdd-native-managed-uptake.test.ts +11 -21
- package/tests/sdd-no-attempts-contract.test.ts +15 -0
- package/tests/sdd-odd-integration.test.ts +33 -0
- package/tests/sdd-optional-research.test.ts +124 -0
- package/tests/sdd-planning-routing-contract.test.ts +1 -1
- package/tests/sdd-preflight-rpc-input.test.ts +125 -0
- package/tests/sdd-preflight.test.ts +1 -1
- package/tests/sdd-research-capabilities.test.ts +20 -162
- package/tests/sdd-selection-transport.test.ts +180 -88
- package/tests/sdd-status.test.ts +5 -778
- package/tests/sdd-task-truth.test.ts +43 -0
- package/tests/session-change-capture.test.ts +20 -2
- package/tests/session-changes.test.ts +11 -0
- package/tests/shell-bar.test.ts +21 -0
- package/tests/shell-card.test.ts +8 -6
- package/tests/shell-changes.test.ts +8 -0
- package/tests/shell-prompt.test.ts +41 -7
- package/tests/shell-sidebar-banner.test.ts +4 -4
- package/tests/shell-sidebar-layout.test.ts +97 -13
- package/tests/startup-banner.test.ts +55 -2
- package/tests/windows-hidden-processes.test.ts +303 -0
- package/tests/windows-session-bootstrap.test.ts +1772 -0
- package/tests/windows-session-compile.test.ts +170 -0
- package/tests/windows-session-transport.test.ts +754 -0
- package/assets/agents/sdd-sync.md +0 -146
- package/lib/openspec-guardrails.ts +0 -99
- package/tests/native-sdd-attempt-authority.test.ts +0 -240
- package/tests/openspec-guardrails.test.ts +0 -71
|
@@ -1,146 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: sdd-sync
|
|
3
|
-
description: Sync verified SDD delta specs into OpenSpec canonical specs without archiving the change.
|
|
4
|
-
tools:
|
|
5
|
-
- read
|
|
6
|
-
- grep
|
|
7
|
-
- find
|
|
8
|
-
- write
|
|
9
|
-
- edit
|
|
10
|
-
- bash
|
|
11
|
-
- mem_search
|
|
12
|
-
- mem_get_observation
|
|
13
|
-
- mem_save
|
|
14
|
-
- mem_update
|
|
15
|
-
---
|
|
16
|
-
|
|
17
|
-
You are the SDD sync executor for Gentle AI.
|
|
18
|
-
|
|
19
|
-
## Parent Preflight Transport
|
|
20
|
-
|
|
21
|
-
Consume the exact `## SDD Session Preflight` block from parent-provided context. It is parent authority, not a prompt to infer or persist defaults. If absent or malformed, return `blocked` without phase work. A delegated RPC child never confirms or persists SDD choices.
|
|
22
|
-
|
|
23
|
-
## Skill Resolution Contract
|
|
24
|
-
|
|
25
|
-
Use your assigned executor/phase skill for this SDD phase. For project/user skills, prefer parent-injected `## Skills to load before work` paths; read those exact `SKILL.md` files before work. Do not independently discover additional project/user skills or the registry during normal runtime.
|
|
26
|
-
|
|
27
|
-
If skill paths are missing, explicit fallback loading is allowed only as degraded self-healing. Report `skill_resolution` as `paths-injected`, `fallback-registry`, `fallback-path`, or `none`; fallbacks mean the parent should pass indexed paths next time.
|
|
28
|
-
|
|
29
|
-
## Memory Contract
|
|
30
|
-
|
|
31
|
-
Read the change artifacts directly from the active backend before syncing; do not wait for the parent to inline them. The parent may pass references and context, but retrieving them is this phase's responsibility.
|
|
32
|
-
|
|
33
|
-
Inputs to read (`engram`/`both`: use the injected Engram memory read tools for the topic key, then fetch the full observation; `openspec`: read the files under `openspec/changes/{change}/`):
|
|
34
|
-
- Core change artifacts: `sdd/{change}/proposal`, `sdd/{change}/spec`, `sdd/{change}/design`, `sdd/{change}/tasks`, and `sdd/{change}/verify-report`.
|
|
35
|
-
|
|
36
|
-
Persist this phase's artifact to the active backend before returning (mandatory):
|
|
37
|
-
- `engram`/`both`: call the injected Engram save tool with title and `topic_key` `"sdd/{change}/sync-report"`, `type: "architecture"`, `project` from context, and `capture_prompt: false` when the tool schema supports it (omit the field if an older schema rejects it).
|
|
38
|
-
- `openspec`: write/update the canonical specs and sync report under `openspec/`.
|
|
39
|
-
- `none`: return the sync report inline.
|
|
40
|
-
|
|
41
|
-
Never claim persistence you did not perform.
|
|
42
|
-
|
|
43
|
-
## Purpose
|
|
44
|
-
|
|
45
|
-
Sync file-backed SDD change specs into canonical `openspec/specs/` without moving the change to archive. This matches the OpenSpec/OPSX distinction between sync and archive:
|
|
46
|
-
|
|
47
|
-
- `sdd-sync`: update canonical specs and keep the change active.
|
|
48
|
-
- `sdd-archive`: verify archive readiness and move the already-synced change to dated archive.
|
|
49
|
-
|
|
50
|
-
## Status and Action Context Guard
|
|
51
|
-
|
|
52
|
-
Before syncing, consume structured SDD status from the parent prompt. If missing, produce the same fields using this lookup order: project override `.pi/gentle-ai/support/sdd-status-contract.md`, then globally installed `~/.pi/agent/gentle-ai/support/sdd-status-contract.md`, then the embedded status contract. Do not use `assets/support/...` as a runtime path; that is only the package source path before installation.
|
|
53
|
-
|
|
54
|
-
**Non-authoritative carve-out:** when native status JSON shows `nextRecommended: "resolve-via-engram"` (covers `artifactStore: engram`, `artifactStore: none`, and `artifactStore: both` without an `openspec/` directory), the status is non-authoritative. Do not treat `dependencies` or `blockedReasons` from that status as real blockers. For `engram` store, refer to the Artifact Store Modes section — sync is not applicable; return a report explaining that canonical spec merge is not supported in Engram-only mode.
|
|
55
|
-
|
|
56
|
-
Stop with `blocked` if:
|
|
57
|
-
|
|
58
|
-
- active change selection is missing or ambiguous;
|
|
59
|
-
- `actionContext.mode: workspace-planning` and no `allowedEditRoots` are provided;
|
|
60
|
-
- canonical spec paths are outside the authoritative workspace or allowed edit roots.
|
|
61
|
-
|
|
62
|
-
## Artifact Store Modes
|
|
63
|
-
|
|
64
|
-
- `openspec`: perform filesystem sync and write `sync-report.md`.
|
|
65
|
-
- `both` / `hybrid`: perform filesystem sync, write `sync-report.md`, and save `sdd/{change}/sync-report` to memory when tools are available.
|
|
66
|
-
- `engram`: do not perform canonical sync. Engram is working memory and has no canonical spec merge layer; return or save a report explaining that sync is not applicable.
|
|
67
|
-
- `none`: return a report only.
|
|
68
|
-
|
|
69
|
-
## Inputs
|
|
70
|
-
|
|
71
|
-
Read:
|
|
72
|
-
|
|
73
|
-
- `openspec/changes/{change}/proposal.md`
|
|
74
|
-
- `openspec/changes/{change}/specs/`
|
|
75
|
-
- `openspec/changes/{change}/tasks.md` when present
|
|
76
|
-
- `openspec/changes/{change}/verify-report.md`
|
|
77
|
-
- `openspec/config.yaml` when present
|
|
78
|
-
|
|
79
|
-
Stop with `blocked` if:
|
|
80
|
-
|
|
81
|
-
- `verify-report.md` is missing;
|
|
82
|
-
- the verification report is not clearly passing, or contains unresolved `FAIL`, `BLOCKED`, `CRITICAL`, or verification blockers;
|
|
83
|
-
- file-backed mode has only legacy flat `openspec/changes/{change}/spec.md` and no domain specs;
|
|
84
|
-
- a MODIFIED or REMOVED requirement does not exist in the canonical spec;
|
|
85
|
-
- a destructive sync uses REMOVED requirements or large MODIFIED blocks and the parent prompt does not record explicit approval;
|
|
86
|
-
- another active change touches the same `specs/{domain}/spec.md` and the parent prompt does not record a chosen archive/sync order;
|
|
87
|
-
- a delta contains `## RENAMED Requirements`; RENAMED sync is not supported by the native helper yet, so require a corrected ADDED/MODIFIED/REMOVED delta or explicit helper implementation before syncing.
|
|
88
|
-
|
|
89
|
-
## File-Backed Sync
|
|
90
|
-
|
|
91
|
-
For each domain spec in:
|
|
92
|
-
|
|
93
|
-
```text
|
|
94
|
-
openspec/changes/{change}/specs/{domain}/spec.md
|
|
95
|
-
```
|
|
96
|
-
|
|
97
|
-
sync into:
|
|
98
|
-
|
|
99
|
-
```text
|
|
100
|
-
openspec/specs/{domain}/spec.md
|
|
101
|
-
```
|
|
102
|
-
|
|
103
|
-
Use the native helper semantics from `lib/openspec-deltas.ts` when editing manually:
|
|
104
|
-
|
|
105
|
-
- If canonical spec does not exist, copy the change spec as the new canonical spec.
|
|
106
|
-
- `## ADDED Requirements` appends requirements.
|
|
107
|
-
- `## MODIFIED Requirements` replaces full matching requirement blocks by exact name.
|
|
108
|
-
- `## REMOVED Requirements` deletes full matching requirement blocks by exact name.
|
|
109
|
-
- `## RENAMED Requirements` is intentionally unsupported until `lib/openspec-deltas.ts` implements it; block instead of improvising.
|
|
110
|
-
- Preserve unrelated canonical requirements and document sections.
|
|
111
|
-
|
|
112
|
-
Use guardrail semantics from `lib/openspec-guardrails.ts`:
|
|
113
|
-
|
|
114
|
-
- warn on active same-domain collisions;
|
|
115
|
-
- detect legacy flat specs;
|
|
116
|
-
- report destructive REMOVED / large MODIFIED deltas and require approval.
|
|
117
|
-
|
|
118
|
-
## Sync Report
|
|
119
|
-
|
|
120
|
-
Write `openspec/changes/{change}/sync-report.md` in file-backed modes.
|
|
121
|
-
|
|
122
|
-
Include:
|
|
123
|
-
|
|
124
|
-
- status: synced / blocked / not-applicable;
|
|
125
|
-
- domains synced;
|
|
126
|
-
- canonical files updated;
|
|
127
|
-
- ADDED/MODIFIED/REMOVED requirement names;
|
|
128
|
-
- active same-domain collisions;
|
|
129
|
-
- destructive sync approvals or blockers;
|
|
130
|
-
- validation commands or checks performed;
|
|
131
|
-
- structured status and `actionContext` findings;
|
|
132
|
-
- next recommended phase: `sdd-archive` when clean.
|
|
133
|
-
|
|
134
|
-
## Rules
|
|
135
|
-
|
|
136
|
-
- Do not move the change folder to archive.
|
|
137
|
-
- Do not commit.
|
|
138
|
-
- Do not launch child subagents. Parent/orchestrator owns delegation.
|
|
139
|
-
- Apply `rules.sync` from `openspec/config.yaml` when present.
|
|
140
|
-
|
|
141
|
-
Return the standard phase envelope with status, executive_summary, artifacts, next_recommended, risks, and skill_resolution.
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
## Key Learnings Closing
|
|
145
|
-
|
|
146
|
-
Close your final report text with a `## Key Learnings` block (no trailing colon). Use 1–5 numbered items, each a standalone factual sentence of at least 20 characters and at least 4 words. This applies to final report text only — not intermediate tool output or saved artifact content. The Engram memory provider automatically extracts and persists these items as passive capture; you do not parse the block or invoke passive-capture tools yourself. Omit the block when there is genuinely no reusable learning; no filler or speculation. This closing block is separate from explicit `mem_save` artifact/decision persistence.
|
|
@@ -1,99 +0,0 @@
|
|
|
1
|
-
import { existsSync, readdirSync, statSync } from "node:fs";
|
|
2
|
-
import { join } from "node:path";
|
|
3
|
-
import { parseDeltaSpec } from "./openspec-deltas.ts";
|
|
4
|
-
|
|
5
|
-
export interface DomainCollision {
|
|
6
|
-
change: string;
|
|
7
|
-
path: string;
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
export interface LegacyFlatSpecWarning {
|
|
11
|
-
change: string;
|
|
12
|
-
path: string;
|
|
13
|
-
hasDomainSpecs: boolean;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
export interface LargeModifiedRequirement {
|
|
17
|
-
name: string;
|
|
18
|
-
lineCount: number;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
export interface DestructiveDeltaReport {
|
|
22
|
-
destructive: boolean;
|
|
23
|
-
removedRequirements: string[];
|
|
24
|
-
largeModifiedRequirements: LargeModifiedRequirement[];
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
export interface DestructiveDeltaOptions {
|
|
28
|
-
largeModifiedLineThreshold?: number;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
function safeDirectories(path: string): string[] {
|
|
32
|
-
try {
|
|
33
|
-
return readdirSync(path).filter((entry) => {
|
|
34
|
-
try {
|
|
35
|
-
return statSync(join(path, entry)).isDirectory();
|
|
36
|
-
} catch {
|
|
37
|
-
return false;
|
|
38
|
-
}
|
|
39
|
-
});
|
|
40
|
-
} catch {
|
|
41
|
-
return [];
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
function hasAnyDomainSpec(specsDir: string): boolean {
|
|
46
|
-
for (const domain of safeDirectories(specsDir)) {
|
|
47
|
-
if (existsSync(join(specsDir, domain, "spec.md"))) return true;
|
|
48
|
-
}
|
|
49
|
-
return false;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
export function detectActiveDomainCollisions(
|
|
53
|
-
cwd: string,
|
|
54
|
-
changeName: string,
|
|
55
|
-
domain: string,
|
|
56
|
-
): DomainCollision[] {
|
|
57
|
-
const changesDir = join(cwd, "openspec", "changes");
|
|
58
|
-
const collisions: DomainCollision[] = [];
|
|
59
|
-
for (const change of safeDirectories(changesDir)) {
|
|
60
|
-
if (change === "archive" || change === changeName) continue;
|
|
61
|
-
const path = join(changesDir, change, "specs", domain, "spec.md");
|
|
62
|
-
if (existsSync(path)) collisions.push({ change, path });
|
|
63
|
-
}
|
|
64
|
-
return collisions;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
export function detectLegacyFlatSpec(
|
|
68
|
-
cwd: string,
|
|
69
|
-
changeName: string,
|
|
70
|
-
): LegacyFlatSpecWarning | undefined {
|
|
71
|
-
const changeDir = join(cwd, "openspec", "changes", changeName);
|
|
72
|
-
const path = join(changeDir, "spec.md");
|
|
73
|
-
if (!existsSync(path)) return undefined;
|
|
74
|
-
return {
|
|
75
|
-
change: changeName,
|
|
76
|
-
path,
|
|
77
|
-
hasDomainSpecs: hasAnyDomainSpec(join(changeDir, "specs")),
|
|
78
|
-
};
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
export function analyzeDeltaDestructiveness(
|
|
82
|
-
deltaMarkdown: string,
|
|
83
|
-
options: DestructiveDeltaOptions = {},
|
|
84
|
-
): DestructiveDeltaReport {
|
|
85
|
-
const threshold = options.largeModifiedLineThreshold ?? 40;
|
|
86
|
-
const delta = parseDeltaSpec(deltaMarkdown);
|
|
87
|
-
const removedRequirements = delta.removed.map((block) => block.name);
|
|
88
|
-
const largeModifiedRequirements = delta.modified
|
|
89
|
-
.map((block) => ({
|
|
90
|
-
name: block.name,
|
|
91
|
-
lineCount: block.content.split("\n").length,
|
|
92
|
-
}))
|
|
93
|
-
.filter((block) => block.lineCount >= threshold);
|
|
94
|
-
return {
|
|
95
|
-
destructive: removedRequirements.length > 0 || largeModifiedRequirements.length > 0,
|
|
96
|
-
removedRequirements,
|
|
97
|
-
largeModifiedRequirements,
|
|
98
|
-
};
|
|
99
|
-
}
|
|
@@ -1,240 +0,0 @@
|
|
|
1
|
-
import assert from "node:assert/strict";
|
|
2
|
-
import { readFileSync } from "node:fs";
|
|
3
|
-
import { join } from "node:path";
|
|
4
|
-
import test from "node:test";
|
|
5
|
-
|
|
6
|
-
// ---------------------------------------------------------------------------
|
|
7
|
-
// Native SDD Runtime Attempt Authority contract tests (issue #256 track 3).
|
|
8
|
-
//
|
|
9
|
-
// Locks the Pi-owned lazy-loaded orchestration/status contract that ports the
|
|
10
|
-
// released Gentle AI v2.4.0 compact SDD attempt ledger authority. Pi must NOT
|
|
11
|
-
// implement a local attempt runtime; it must route every runtime-bearing
|
|
12
|
-
// sdd-apply/sdd-verify/remediation launch through the provider compact CLI.
|
|
13
|
-
//
|
|
14
|
-
// Both assets are Markdown contracts consumed by orchestrators; these tests
|
|
15
|
-
// read the real repo files and assert the essential semantics, not merely
|
|
16
|
-
// keyword presence. Negative controls guard against caller-authored
|
|
17
|
-
// counters, OpenSpec/Engram attempt ledgers, automatic reset, and legacy
|
|
18
|
-
// status|begin|finish|reset normal-flow routing.
|
|
19
|
-
// ---------------------------------------------------------------------------
|
|
20
|
-
|
|
21
|
-
const ROOT = join(import.meta.dirname, "..");
|
|
22
|
-
const WORKFLOW = "assets/sdd-orchestrator-workflow.md";
|
|
23
|
-
const STATUS_CONTRACT = "assets/support/sdd-status-contract.md";
|
|
24
|
-
const SECTION = "Native Runtime Attempt Authority";
|
|
25
|
-
|
|
26
|
-
function read(path: string): string {
|
|
27
|
-
return readFileSync(join(ROOT, path), "utf8");
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
function readMarkdownSection(source: string, heading: string): string {
|
|
31
|
-
const lines = source.split(/\r?\n/);
|
|
32
|
-
const matches = lines.flatMap((line, index) => {
|
|
33
|
-
const match = line.match(/^(#{1,6})\s+(.+?)\s*$/);
|
|
34
|
-
return match?.[2] === heading ? [{ index, level: match[1].length }] : [];
|
|
35
|
-
});
|
|
36
|
-
assert.equal(
|
|
37
|
-
matches.length,
|
|
38
|
-
1,
|
|
39
|
-
`Markdown must contain exactly one "${heading}" section (found ${matches.length})`,
|
|
40
|
-
);
|
|
41
|
-
const [{ index: start, level }] = matches;
|
|
42
|
-
const relativeEnd = lines.slice(start + 1).findIndex((line) => {
|
|
43
|
-
const match = line.match(/^(#{1,6})\s+/);
|
|
44
|
-
return match !== null && match[1].length <= level;
|
|
45
|
-
});
|
|
46
|
-
const end = relativeEnd === -1 ? lines.length : start + 1 + relativeEnd;
|
|
47
|
-
return lines.slice(start + 1, end).join("\n").trim();
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
// Extracts one command line beginning with `commandPrefix` so payload args bind
|
|
51
|
-
// to the correct command shape; `--cwd`, `--change`, `--request-id` appear in
|
|
52
|
-
// both acquire and settle, so a section-wide match stays green on arg removal.
|
|
53
|
-
function extractCommandLine(section: string, commandPrefix: string): string {
|
|
54
|
-
const escaped = commandPrefix.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
55
|
-
const re = new RegExp(`^[ \t]*${escaped}.*$`, "gm");
|
|
56
|
-
const matches = section.match(re) ?? [];
|
|
57
|
-
assert.equal(
|
|
58
|
-
matches.length,
|
|
59
|
-
1,
|
|
60
|
-
`Native Runtime Attempt Authority section must contain exactly one command line starting with "${commandPrefix}" (found ${matches.length})`,
|
|
61
|
-
);
|
|
62
|
-
const [line] = matches;
|
|
63
|
-
return line;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
test("extractCommandLine rejects duplicate matching command lines (negative control)", () => {
|
|
67
|
-
for (const prefix of [
|
|
68
|
-
"gentle-ai sdd-attempt acquire ",
|
|
69
|
-
"gentle-ai sdd-attempt settle ",
|
|
70
|
-
]) {
|
|
71
|
-
const duplicate = [
|
|
72
|
-
`${prefix}--cwd repo --change change --request-id id`,
|
|
73
|
-
`${prefix}--cwd repo2 --change change2 --request-id id2`,
|
|
74
|
-
].join("\n");
|
|
75
|
-
assert.throws(
|
|
76
|
-
() => extractCommandLine(duplicate, prefix),
|
|
77
|
-
/exactly one command line/,
|
|
78
|
-
`duplicate ${prefix.trim()} lines must be rejected`,
|
|
79
|
-
);
|
|
80
|
-
}
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
test("workflow asset has exactly one Native Runtime Attempt Authority section", () => {
|
|
84
|
-
const section = readMarkdownSection(read(WORKFLOW), SECTION);
|
|
85
|
-
assert.ok(section.length > 0, "section must be non-empty");
|
|
86
|
-
});
|
|
87
|
-
|
|
88
|
-
test("status contract asset has exactly one Native Runtime Attempt Authority section", () => {
|
|
89
|
-
const section = readMarkdownSection(read(STATUS_CONTRACT), SECTION);
|
|
90
|
-
assert.ok(section.length > 0, "section must be non-empty");
|
|
91
|
-
});
|
|
92
|
-
|
|
93
|
-
test("workflow names acquire and settle as sole provider authority for OpenSpec and Engram", () => {
|
|
94
|
-
const section = readMarkdownSection(read(WORKFLOW), SECTION);
|
|
95
|
-
assert.match(section, /sdd-attempt acquire/);
|
|
96
|
-
assert.match(section, /sdd-attempt settle/);
|
|
97
|
-
assert.match(section, /sole attempt and changed-line budget authority/i);
|
|
98
|
-
assert.match(section, /Git-common-dir/);
|
|
99
|
-
assert.match(section, /OpenSpec and Engram/);
|
|
100
|
-
});
|
|
101
|
-
|
|
102
|
-
test("status contract names acquire and settle and forbids OpenSpec/Engram attempt ledgers", () => {
|
|
103
|
-
const section = readMarkdownSection(read(STATUS_CONTRACT), SECTION);
|
|
104
|
-
assert.match(section, /acquire/);
|
|
105
|
-
assert.match(section, /settle/);
|
|
106
|
-
assert.match(section, /OpenSpec or Engram attempt ledger/i);
|
|
107
|
-
});
|
|
108
|
-
|
|
109
|
-
test("workflow launches only on proceed; blocked and complete stop the launch", () => {
|
|
110
|
-
const section = readMarkdownSection(read(WORKFLOW), SECTION);
|
|
111
|
-
assert.match(section, /proceed/);
|
|
112
|
-
assert.match(section, /blocked/);
|
|
113
|
-
assert.match(section, /complete/);
|
|
114
|
-
assert.match(section, /launch only on `proceed`/i);
|
|
115
|
-
assert.match(section, /blocked.*do not launch/i);
|
|
116
|
-
assert.match(section, /complete.*do not launch/i);
|
|
117
|
-
});
|
|
118
|
-
|
|
119
|
-
test("workflow requires distinct request IDs with own-ID-only idempotent replay", () => {
|
|
120
|
-
const section = readMarkdownSection(read(WORKFLOW), SECTION);
|
|
121
|
-
assert.match(section, /distinct from acquire/i);
|
|
122
|
-
assert.match(section, /own ID only for idempotent replay/i);
|
|
123
|
-
});
|
|
124
|
-
|
|
125
|
-
test("workflow acquire command line binds every mandatory payload argument", () => {
|
|
126
|
-
const section = readMarkdownSection(read(WORKFLOW), SECTION);
|
|
127
|
-
const acquire = extractCommandLine(section, "gentle-ai sdd-attempt acquire ");
|
|
128
|
-
for (const arg of [
|
|
129
|
-
"--cwd <repo>",
|
|
130
|
-
"--change <change>",
|
|
131
|
-
"--request-id <id>",
|
|
132
|
-
"--work-unit <label>",
|
|
133
|
-
"--evidence-goal <goal>",
|
|
134
|
-
"--max-attempts <count>",
|
|
135
|
-
"--max-changed-lines <count>",
|
|
136
|
-
]) {
|
|
137
|
-
assert.ok(acquire.includes(arg), `acquire command is missing ${arg}; command: ${acquire}`);
|
|
138
|
-
}
|
|
139
|
-
});
|
|
140
|
-
|
|
141
|
-
test("workflow settle command line binds mandatory arguments and routing invariants", () => {
|
|
142
|
-
const section = readMarkdownSection(read(WORKFLOW), SECTION);
|
|
143
|
-
const settle = extractCommandLine(section, "gentle-ai sdd-attempt settle ");
|
|
144
|
-
for (const arg of [
|
|
145
|
-
"--cwd <repo>",
|
|
146
|
-
"--change <change>",
|
|
147
|
-
"--token <token>",
|
|
148
|
-
"--request-id <id>",
|
|
149
|
-
"--outcome <failed|interrupted|passed>",
|
|
150
|
-
"--diagnosis <text>",
|
|
151
|
-
"--harness-disposition <reused|invalidated>",
|
|
152
|
-
"--cleanup-evidence <text>",
|
|
153
|
-
"--process-evidence <text>",
|
|
154
|
-
]) {
|
|
155
|
-
assert.ok(settle.includes(arg), `settle command is missing ${arg}; command: ${settle}`);
|
|
156
|
-
}
|
|
157
|
-
assert.ok(settle.includes("[--evidence-revision <sha256:...>]"));
|
|
158
|
-
assert.match(section, /Every settle field except `evidence-revision` is required/);
|
|
159
|
-
assert.match(
|
|
160
|
-
section,
|
|
161
|
-
/For `failed` or `passed`, include `--evidence-revision` with the `sha256:\.\.\.` evidence hash\./,
|
|
162
|
-
);
|
|
163
|
-
assert.match(section, /For `interrupted`, omit the entire `--evidence-revision` flag\./);
|
|
164
|
-
assert.match(section, /proceed\|blocked\|complete/);
|
|
165
|
-
assert.match(section, /--successor-lineage/);
|
|
166
|
-
assert.match(section, /--remediates-evidence-revision/);
|
|
167
|
-
});
|
|
168
|
-
|
|
169
|
-
test("workflow forbids caller-authored counters and Pi-owned attempt state", () => {
|
|
170
|
-
const section = readMarkdownSection(read(WORKFLOW), SECTION);
|
|
171
|
-
assert.match(section, /never persist caller-authored/i);
|
|
172
|
-
assert.match(section, /OpenSpec artifacts, Engram memory/);
|
|
173
|
-
assert.match(section, /counter|token store|state machine|interception/i);
|
|
174
|
-
});
|
|
175
|
-
|
|
176
|
-
test("workflow states reset is never automatic and names compatibility surfaces", () => {
|
|
177
|
-
const section = readMarkdownSection(read(WORKFLOW), SECTION);
|
|
178
|
-
assert.match(section, /`status`, `begin`, `finish`, and `reset`/);
|
|
179
|
-
assert.match(section, /diagnostic\/compatibility surfaces/);
|
|
180
|
-
assert.match(section, /not the normal runtime route/i);
|
|
181
|
-
assert.match(section, /reset.*never automatic/i);
|
|
182
|
-
assert.match(section, /explicit maintainer scope decision/);
|
|
183
|
-
});
|
|
184
|
-
|
|
185
|
-
test("workflow gatekeeper rerun is subordinate to a fresh native acquire", () => {
|
|
186
|
-
const section = readMarkdownSection(read(WORKFLOW), SECTION);
|
|
187
|
-
assert.match(section, /rerun never bypasses native attempt authority/i);
|
|
188
|
-
assert.match(section, /fresh compact acquire/i);
|
|
189
|
-
// The gatekeeper quality rule is preserved (not deleted).
|
|
190
|
-
const gatekeeper = readMarkdownSection(read(WORKFLOW), "Automatic Mode Gatekeeper");
|
|
191
|
-
assert.match(gatekeeper, /rerun the same SDD phase once with corrective feedback/);
|
|
192
|
-
assert.match(gatekeeper, /Validate the rerun/);
|
|
193
|
-
});
|
|
194
|
-
|
|
195
|
-
test("workflow does not present status|begin|finish|reset as the primary route", () => {
|
|
196
|
-
const section = readMarkdownSection(read(WORKFLOW), SECTION);
|
|
197
|
-
assert.match(section, /not the normal runtime route/i);
|
|
198
|
-
assert.doesNotMatch(
|
|
199
|
-
section,
|
|
200
|
-
/primary route.*sdd-attempt (?:status|begin|finish|reset)/i,
|
|
201
|
-
);
|
|
202
|
-
});
|
|
203
|
-
|
|
204
|
-
test("status contract authority is artifact-store agnostic and excluded from SDD v1 status", () => {
|
|
205
|
-
const section = readMarkdownSection(read(STATUS_CONTRACT), SECTION);
|
|
206
|
-
assert.match(section, /artifact-store agnostic/i);
|
|
207
|
-
assert.match(section, /MUST NOT be embedded in the SDD v1 status/i);
|
|
208
|
-
assert.match(section, /separate from artifact dispatch/i);
|
|
209
|
-
});
|
|
210
|
-
|
|
211
|
-
test("status contract continuation rule: acquire before launch, settle after run, distinct IDs, provider states route, reset never automatic", () => {
|
|
212
|
-
const section = readMarkdownSection(read(STATUS_CONTRACT), SECTION);
|
|
213
|
-
assert.match(section, /before every runtime-bearing/i);
|
|
214
|
-
assert.match(section, /after the external run completes it MUST settle/i);
|
|
215
|
-
assert.match(section, /distinct/i);
|
|
216
|
-
assert.match(section, /proceed\|blocked\|complete/);
|
|
217
|
-
assert.match(section, /launch only on `proceed`/i);
|
|
218
|
-
assert.match(section, /reset.*never automatic/i);
|
|
219
|
-
});
|
|
220
|
-
|
|
221
|
-
test("status contract schema is unchanged (no attempt authority payload added)", () => {
|
|
222
|
-
const schema = readMarkdownSection(read(STATUS_CONTRACT), "Status Schema");
|
|
223
|
-
assert.doesNotMatch(
|
|
224
|
-
schema,
|
|
225
|
-
/attemptToken|attempt_token|attemptCount|attempt_counter|sddAttempt|sdd_attempt|nativeAttempt/i,
|
|
226
|
-
"Status Schema must not embed the runtime attempt authority payload",
|
|
227
|
-
);
|
|
228
|
-
});
|
|
229
|
-
|
|
230
|
-
test("status contract uses a runtime-safe semantic reference, not a repo-source assets/ path", () => {
|
|
231
|
-
const section = readMarkdownSection(read(STATUS_CONTRACT), SECTION);
|
|
232
|
-
assert.doesNotMatch(
|
|
233
|
-
section,
|
|
234
|
-
/See `assets\/sdd-orchestrator-workflow\.md`|see `assets\//i,
|
|
235
|
-
"status contract must not point installed consumers at a repo-source assets/ path; those are package source paths before installation",
|
|
236
|
-
);
|
|
237
|
-
assert.match(section, /lazy-loaded `SDD Orchestrator Workflow`/i);
|
|
238
|
-
assert.match(section, /Native Runtime Attempt Authority/);
|
|
239
|
-
assert.match(section, /Do not look up `assets\/\.\.\.` paths at runtime/i);
|
|
240
|
-
});
|
|
@@ -1,71 +0,0 @@
|
|
|
1
|
-
import assert from "node:assert/strict";
|
|
2
|
-
import { mkdirSync, writeFileSync } from "node:fs";
|
|
3
|
-
import { mkdtemp } from "node:fs/promises";
|
|
4
|
-
import { tmpdir } from "node:os";
|
|
5
|
-
import { join } from "node:path";
|
|
6
|
-
import test from "node:test";
|
|
7
|
-
import {
|
|
8
|
-
analyzeDeltaDestructiveness,
|
|
9
|
-
detectActiveDomainCollisions,
|
|
10
|
-
detectLegacyFlatSpec,
|
|
11
|
-
} from "../lib/openspec-guardrails.ts";
|
|
12
|
-
|
|
13
|
-
test("detectActiveDomainCollisions finds other active changes touching the same domain", async () => {
|
|
14
|
-
const cwd = await mkdtemp(join(tmpdir(), "gentle-pi-guardrails-"));
|
|
15
|
-
mkdirSync(join(cwd, "openspec/changes/current/specs/sdd-openspec"), { recursive: true });
|
|
16
|
-
mkdirSync(join(cwd, "openspec/changes/other/specs/sdd-openspec"), { recursive: true });
|
|
17
|
-
mkdirSync(join(cwd, "openspec/changes/archive/2026-01-01-old/specs/sdd-openspec"), { recursive: true });
|
|
18
|
-
writeFileSync(join(cwd, "openspec/changes/current/specs/sdd-openspec/spec.md"), "# Current\n");
|
|
19
|
-
writeFileSync(join(cwd, "openspec/changes/other/specs/sdd-openspec/spec.md"), "# Other\n");
|
|
20
|
-
writeFileSync(join(cwd, "openspec/changes/archive/2026-01-01-old/specs/sdd-openspec/spec.md"), "# Old\n");
|
|
21
|
-
|
|
22
|
-
const collisions = detectActiveDomainCollisions(cwd, "current", "sdd-openspec");
|
|
23
|
-
|
|
24
|
-
assert.deepEqual(collisions.map((collision) => collision.change), ["other"]);
|
|
25
|
-
assert.match(collisions[0].path, /openspec\/changes\/other\/specs\/sdd-openspec\/spec\.md$/);
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
test("detectLegacyFlatSpec warns when a flat change spec exists without domain specs", async () => {
|
|
29
|
-
const cwd = await mkdtemp(join(tmpdir(), "gentle-pi-legacy-flat-"));
|
|
30
|
-
mkdirSync(join(cwd, "openspec/changes/legacy-change"), { recursive: true });
|
|
31
|
-
writeFileSync(join(cwd, "openspec/changes/legacy-change/spec.md"), "# Legacy\n");
|
|
32
|
-
|
|
33
|
-
assert.deepEqual(detectLegacyFlatSpec(cwd, "legacy-change"), {
|
|
34
|
-
change: "legacy-change",
|
|
35
|
-
path: join(cwd, "openspec/changes/legacy-change/spec.md"),
|
|
36
|
-
hasDomainSpecs: false,
|
|
37
|
-
});
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
test("detectLegacyFlatSpec reports domain specs when both old and new layouts exist", async () => {
|
|
41
|
-
const cwd = await mkdtemp(join(tmpdir(), "gentle-pi-legacy-both-"));
|
|
42
|
-
mkdirSync(join(cwd, "openspec/changes/mixed/specs/domain"), { recursive: true });
|
|
43
|
-
writeFileSync(join(cwd, "openspec/changes/mixed/spec.md"), "# Legacy\n");
|
|
44
|
-
writeFileSync(join(cwd, "openspec/changes/mixed/specs/domain/spec.md"), "# Domain\n");
|
|
45
|
-
|
|
46
|
-
assert.equal(detectLegacyFlatSpec(cwd, "mixed")?.hasDomainSpecs, true);
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
test("analyzeDeltaDestructiveness reports removed and large modified requirements", () => {
|
|
50
|
-
const report = analyzeDeltaDestructiveness(
|
|
51
|
-
`# Delta
|
|
52
|
-
|
|
53
|
-
## MODIFIED Requirements
|
|
54
|
-
|
|
55
|
-
### Requirement: Big Replacement
|
|
56
|
-
|
|
57
|
-
${Array.from({ length: 15 }, (_, index) => `Line ${index + 1}`).join("\n")}
|
|
58
|
-
|
|
59
|
-
## REMOVED Requirements
|
|
60
|
-
|
|
61
|
-
### Requirement: Removed Behavior
|
|
62
|
-
|
|
63
|
-
(Reason: no longer supported)
|
|
64
|
-
`,
|
|
65
|
-
{ largeModifiedLineThreshold: 10 },
|
|
66
|
-
);
|
|
67
|
-
|
|
68
|
-
assert.equal(report.destructive, true);
|
|
69
|
-
assert.deepEqual(report.removedRequirements, ["Removed Behavior"]);
|
|
70
|
-
assert.deepEqual(report.largeModifiedRequirements.map((item) => item.name), ["Big Replacement"]);
|
|
71
|
-
});
|