clawspec 1.0.5 → 1.0.7
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/package.json +1 -1
- package/src/orchestrator/service.ts +64 -11
- package/src/utils/paths.ts +2 -0
- package/src/worker/prompts.ts +44 -8
- package/test/helpers/harness.ts +21 -0
- package/test/queue-planning.test.ts +13 -1
package/package.json
CHANGED
|
@@ -23,6 +23,7 @@ import type {
|
|
|
23
23
|
ExecutionResult,
|
|
24
24
|
OpenSpecApplyInstructionsResponse,
|
|
25
25
|
OpenSpecCommandResult,
|
|
26
|
+
OpenSpecInstructionsResponse,
|
|
26
27
|
OpenSpecStatusResponse,
|
|
27
28
|
ProjectState,
|
|
28
29
|
TaskCountSummary,
|
|
@@ -566,6 +567,7 @@ export class ClawSpecService {
|
|
|
566
567
|
private async buildPlanningSyncInjection(
|
|
567
568
|
project: ProjectState,
|
|
568
569
|
userPrompt: string,
|
|
570
|
+
instructionResults: Array<OpenSpecCommandResult<OpenSpecInstructionsResponse>>,
|
|
569
571
|
): Promise<{ prependContext?: string; prependSystemContext?: string }> {
|
|
570
572
|
const repoStatePaths = getRepoStatePaths(project.repoPath!, this.archiveDirName);
|
|
571
573
|
await this.ensureProjectSupportFiles(project);
|
|
@@ -584,13 +586,20 @@ export class ClawSpecService {
|
|
|
584
586
|
contextPaths: planningContext.paths,
|
|
585
587
|
scaffoldOnly: planningContext.scaffoldOnly,
|
|
586
588
|
mode: "sync",
|
|
589
|
+
prefetchedInstructions: instructionResults.map((result) => result.parsed!).filter(Boolean),
|
|
590
|
+
prefetchedInstructionCommands: instructionResults.map((result) => result.command).filter(Boolean),
|
|
587
591
|
}),
|
|
588
592
|
};
|
|
589
593
|
}
|
|
590
594
|
|
|
591
595
|
private async preparePlanningSync(channelKey: string): Promise<
|
|
592
596
|
| { result: PluginCommandResult }
|
|
593
|
-
| {
|
|
597
|
+
| {
|
|
598
|
+
project: ProjectState;
|
|
599
|
+
outputs: OpenSpecCommandResult[];
|
|
600
|
+
repoStatePaths: RepoStatePaths;
|
|
601
|
+
instructionResults: Array<OpenSpecCommandResult<OpenSpecInstructionsResponse>>;
|
|
602
|
+
}
|
|
594
603
|
> {
|
|
595
604
|
const project = await this.requireActiveProject(channelKey);
|
|
596
605
|
if (!project.repoPath || !project.projectName || !project.changeName) {
|
|
@@ -661,6 +670,21 @@ export class ClawSpecService {
|
|
|
661
670
|
}
|
|
662
671
|
const statusResult = await this.openSpec.status(project.repoPath, project.changeName);
|
|
663
672
|
outputs.push(statusResult);
|
|
673
|
+
const instructionResults = await this.refreshPlanningInstructionFiles(project, repoStatePaths);
|
|
674
|
+
outputs.push(...instructionResults);
|
|
675
|
+
await this.writeLatestSummary(
|
|
676
|
+
repoStatePaths,
|
|
677
|
+
`Planning instructions refreshed for ${project.changeName} via OpenSpec CLI.`,
|
|
678
|
+
);
|
|
679
|
+
await removeIfExists(repoStatePaths.executionControlFile);
|
|
680
|
+
await removeIfExists(repoStatePaths.executionResultFile);
|
|
681
|
+
await removeIfExists(repoStatePaths.workerProgressFile);
|
|
682
|
+
return {
|
|
683
|
+
project,
|
|
684
|
+
outputs,
|
|
685
|
+
repoStatePaths,
|
|
686
|
+
instructionResults,
|
|
687
|
+
};
|
|
664
688
|
} catch (error) {
|
|
665
689
|
if (error instanceof OpenSpecCommandError) {
|
|
666
690
|
return {
|
|
@@ -677,15 +701,6 @@ export class ClawSpecService {
|
|
|
677
701
|
}
|
|
678
702
|
throw error;
|
|
679
703
|
}
|
|
680
|
-
|
|
681
|
-
await removeIfExists(repoStatePaths.executionControlFile);
|
|
682
|
-
await removeIfExists(repoStatePaths.executionResultFile);
|
|
683
|
-
await removeIfExists(repoStatePaths.workerProgressFile);
|
|
684
|
-
return {
|
|
685
|
-
project,
|
|
686
|
-
outputs,
|
|
687
|
-
repoStatePaths,
|
|
688
|
-
};
|
|
689
704
|
}
|
|
690
705
|
|
|
691
706
|
private async startVisiblePlanningSync(
|
|
@@ -717,7 +732,34 @@ export class ClawSpecService {
|
|
|
717
732
|
lastExecutionAt: startedAt,
|
|
718
733
|
}));
|
|
719
734
|
|
|
720
|
-
return await this.buildPlanningSyncInjection(runningProject, userPrompt);
|
|
735
|
+
return await this.buildPlanningSyncInjection(runningProject, userPrompt, prepared.instructionResults);
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
private async refreshPlanningInstructionFiles(
|
|
739
|
+
project: ProjectState,
|
|
740
|
+
repoStatePaths: RepoStatePaths,
|
|
741
|
+
): Promise<Array<OpenSpecCommandResult<OpenSpecInstructionsResponse>>> {
|
|
742
|
+
if (!project.repoPath || !project.changeName) {
|
|
743
|
+
return [];
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
const artifactIds = ["proposal", "specs", "design", "tasks"] as const;
|
|
747
|
+
await ensureDir(repoStatePaths.planningInstructionsRoot);
|
|
748
|
+
const results: Array<OpenSpecCommandResult<OpenSpecInstructionsResponse>> = [];
|
|
749
|
+
|
|
750
|
+
for (const artifactId of artifactIds) {
|
|
751
|
+
const result = await this.openSpec.instructionsArtifact(project.repoPath, artifactId, project.changeName);
|
|
752
|
+
results.push(result);
|
|
753
|
+
await writeJsonFile(path.join(repoStatePaths.planningInstructionsRoot, `${artifactId}.json`), {
|
|
754
|
+
generatedAt: new Date().toISOString(),
|
|
755
|
+
command: result.command,
|
|
756
|
+
cwd: result.cwd,
|
|
757
|
+
durationMs: result.durationMs,
|
|
758
|
+
instruction: result.parsed,
|
|
759
|
+
});
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
return results;
|
|
721
763
|
}
|
|
722
764
|
|
|
723
765
|
private async collectPlanningContextPaths(
|
|
@@ -728,6 +770,17 @@ export class ClawSpecService {
|
|
|
728
770
|
repoStatePaths.stateFile,
|
|
729
771
|
repoStatePaths.planningJournalFile,
|
|
730
772
|
];
|
|
773
|
+
const instructionFiles = [
|
|
774
|
+
path.join(repoStatePaths.planningInstructionsRoot, "proposal.json"),
|
|
775
|
+
path.join(repoStatePaths.planningInstructionsRoot, "specs.json"),
|
|
776
|
+
path.join(repoStatePaths.planningInstructionsRoot, "design.json"),
|
|
777
|
+
path.join(repoStatePaths.planningInstructionsRoot, "tasks.json"),
|
|
778
|
+
];
|
|
779
|
+
for (const instructionFile of instructionFiles) {
|
|
780
|
+
if (await pathExists(instructionFile)) {
|
|
781
|
+
paths.push(instructionFile);
|
|
782
|
+
}
|
|
783
|
+
}
|
|
731
784
|
|
|
732
785
|
if (!project.changeDir) {
|
|
733
786
|
return {
|
package/src/utils/paths.ts
CHANGED
|
@@ -14,6 +14,7 @@ export type RepoStatePaths = {
|
|
|
14
14
|
latestSummaryFile: string;
|
|
15
15
|
planningJournalFile: string;
|
|
16
16
|
planningJournalSnapshotFile: string;
|
|
17
|
+
planningInstructionsRoot: string;
|
|
17
18
|
rollbackManifestFile: string;
|
|
18
19
|
snapshotsRoot: string;
|
|
19
20
|
archivesRoot: string;
|
|
@@ -75,6 +76,7 @@ export function getRepoStatePaths(repoPath: string, archiveDirName: string): Rep
|
|
|
75
76
|
latestSummaryFile: path.join(root, "latest-summary.md"),
|
|
76
77
|
planningJournalFile: path.join(root, "planning-journal.jsonl"),
|
|
77
78
|
planningJournalSnapshotFile: path.join(root, "planning-journal.snapshot.json"),
|
|
79
|
+
planningInstructionsRoot: path.join(root, "planning-instructions"),
|
|
78
80
|
rollbackManifestFile: path.join(root, "rollback-manifest.json"),
|
|
79
81
|
snapshotsRoot: path.join(root, "snapshots"),
|
|
80
82
|
archivesRoot: path.join(root, archiveDirName),
|
package/src/worker/prompts.ts
CHANGED
|
@@ -139,6 +139,7 @@ export function buildExecutionPrependContext(params: {
|
|
|
139
139
|
"4. If planning-journal state is dirty or planning artifacts are missing, sync `proposal`, `specs`, `design`, and `tasks` in order using `openspec instructions <artifact> --change <name> --json`.",
|
|
140
140
|
"5. After planning sync, run `openspec instructions apply --change <name> --json`, read the returned context files, and use that instruction as the implementation guide.",
|
|
141
141
|
"6. Execute unchecked tasks from tasks.md sequentially. Each time a task is fully complete, update its checkbox from `- [ ]` to `- [x]` immediately.",
|
|
142
|
+
"6.1 For code-change tasks, add or update automated tests before marking the task done, and report the test command/result in progress updates.",
|
|
142
143
|
"7. Between artifacts and tasks, re-check execution-control.json for pauseRequested or cancelRequested.",
|
|
143
144
|
"8. Keep OpenSpec command activity visible by running those commands normally in this chat.",
|
|
144
145
|
"9. Keep the user informed in this chat with explicit progress messages.",
|
|
@@ -177,6 +178,8 @@ export function buildPlanningPrependContext(params: {
|
|
|
177
178
|
scaffoldOnly?: boolean;
|
|
178
179
|
mode: "discussion" | "sync";
|
|
179
180
|
nextActionHint?: "plan" | "work";
|
|
181
|
+
prefetchedInstructions?: OpenSpecInstructionsResponse[];
|
|
182
|
+
prefetchedInstructionCommands?: string[];
|
|
180
183
|
}): string {
|
|
181
184
|
const project = params.project;
|
|
182
185
|
|
|
@@ -185,16 +188,18 @@ export function buildPlanningPrependContext(params: {
|
|
|
185
188
|
"Required workflow for this turn:",
|
|
186
189
|
"0. The active change directory shown above is the only OpenSpec change directory you may inspect or modify in this turn.",
|
|
187
190
|
"1. Read planning-journal.jsonl, .openspec.yaml, and any planning artifacts that already exist.",
|
|
188
|
-
"2.
|
|
189
|
-
"3.
|
|
190
|
-
"4.
|
|
191
|
-
"5. If artifacts are missing or stale,
|
|
192
|
-
"6.
|
|
191
|
+
"2. Treat the prefetched OpenSpec instruction files in this context as the authoritative source for artifact structure, output paths, and writing guidance.",
|
|
192
|
+
"3. Use the current visible chat context plus the planning journal to decide whether there are substantive new requirements, constraints, or design changes since the last planning sync.",
|
|
193
|
+
"4. If there is no substantive planning change, say so clearly in chat and do not rewrite artifacts unnecessarily.",
|
|
194
|
+
"5. If artifacts are missing or stale, update `proposal`, `specs`, `design`, and `tasks` in dependency order using those prefetched OpenSpec instruction files.",
|
|
195
|
+
"6. Do not generate or rewrite planning artifacts from ad-hoc structure guesses; follow OpenSpec instruction/template constraints only.",
|
|
193
196
|
"7. Before updating each artifact, post a short chat update naming the artifact you are about to refresh.",
|
|
194
197
|
"8. After updating each artifact, post a short chat update describing what changed and what artifact comes next.",
|
|
195
|
-
"9.
|
|
196
|
-
"10.
|
|
197
|
-
"11.
|
|
198
|
+
"9. For any implementation-oriented task item, ensure tasks.md contains an explicit testing task (new tests or updated tests) and a validation command.",
|
|
199
|
+
"10. Stop after planning artifacts are refreshed and apply-ready. Do not implement code in this turn.",
|
|
200
|
+
"11. End with a concise summary and a mandatory final line exactly in this shape: `Next: run `cs-work` to start implementation.`",
|
|
201
|
+
"12. Never scan sibling directories under `openspec/changes`, never switch to another change, and never restore or rewrite unrelated files.",
|
|
202
|
+
"13. Do not claim that OpenSpec instructions were skipped in this sync turn. The plugin already executed those commands before this turn began.",
|
|
198
203
|
]
|
|
199
204
|
: [
|
|
200
205
|
"Discussion rules for this turn:",
|
|
@@ -232,6 +237,23 @@ export function buildPlanningPrependContext(params: {
|
|
|
232
237
|
"",
|
|
233
238
|
"Read these files before responding:",
|
|
234
239
|
...params.contextPaths.map((contextPath) => `- ${contextPath}`),
|
|
240
|
+
params.mode === "sync" ? "" : "",
|
|
241
|
+
params.mode === "sync" ? "Prefetched OpenSpec instructions for this turn:" : "",
|
|
242
|
+
...(params.mode === "sync"
|
|
243
|
+
? (params.prefetchedInstructions ?? [])
|
|
244
|
+
.map((instruction) =>
|
|
245
|
+
`- ${instruction.artifactId}: ${displayPath(resolveProjectScopedPath(project, instruction.outputPath))}`,
|
|
246
|
+
)
|
|
247
|
+
: []),
|
|
248
|
+
...(params.mode === "sync"
|
|
249
|
+
? (params.prefetchedInstructions ?? []).flatMap((instruction) => [
|
|
250
|
+
"",
|
|
251
|
+
formatPrefetchedInstructionBlock(project, instruction),
|
|
252
|
+
])
|
|
253
|
+
: []),
|
|
254
|
+
params.mode === "sync" ? "" : "",
|
|
255
|
+
params.mode === "sync" ? "OpenSpec commands already executed by the plugin before this turn:" : "",
|
|
256
|
+
...(params.mode === "sync" ? (params.prefetchedInstructionCommands ?? []).map((command) => `- ${command}`) : []),
|
|
235
257
|
params.scaffoldOnly ? "" : "",
|
|
236
258
|
params.scaffoldOnly ? "Only the change scaffold exists right now. That is expected before planning sync generates the first artifacts." : "",
|
|
237
259
|
"",
|
|
@@ -492,3 +514,17 @@ function relativeChangeFile(project: ProjectState, targetPath: string): string {
|
|
|
492
514
|
function displayPath(targetPath: string): string {
|
|
493
515
|
return targetPath.split(path.sep).join("/");
|
|
494
516
|
}
|
|
517
|
+
|
|
518
|
+
function formatPrefetchedInstructionBlock(
|
|
519
|
+
project: ProjectState,
|
|
520
|
+
instruction: OpenSpecInstructionsResponse,
|
|
521
|
+
): string {
|
|
522
|
+
const outputPath = displayPath(resolveProjectScopedPath(project, instruction.outputPath));
|
|
523
|
+
return [
|
|
524
|
+
`Artifact ${instruction.artifactId} (output: ${outputPath})`,
|
|
525
|
+
"Instruction:",
|
|
526
|
+
fence(instruction.instruction || "(empty)", "markdown"),
|
|
527
|
+
"Template:",
|
|
528
|
+
fence(instruction.template || "(empty)", "markdown"),
|
|
529
|
+
].join("\n");
|
|
530
|
+
}
|
package/test/helpers/harness.ts
CHANGED
|
@@ -133,6 +133,27 @@ export async function createServiceHarness(prefix: string): Promise<{
|
|
|
133
133
|
],
|
|
134
134
|
},
|
|
135
135
|
}),
|
|
136
|
+
instructionsArtifact: async (cwd: string, artifactId: string, changeName: string) => ({
|
|
137
|
+
command: `openspec instructions ${artifactId} --change ${changeName} --json`,
|
|
138
|
+
cwd,
|
|
139
|
+
stdout: "{}",
|
|
140
|
+
stderr: "",
|
|
141
|
+
durationMs: 1,
|
|
142
|
+
parsed: {
|
|
143
|
+
changeName,
|
|
144
|
+
artifactId,
|
|
145
|
+
schemaName: "spec-driven",
|
|
146
|
+
changeDir,
|
|
147
|
+
outputPath: artifactId === "specs"
|
|
148
|
+
? path.join(changeDir, "specs", "demo-spec", "spec.md")
|
|
149
|
+
: path.join(changeDir, `${artifactId}.md`),
|
|
150
|
+
description: `Refresh ${artifactId}`,
|
|
151
|
+
instruction: `Use ${artifactId} template`,
|
|
152
|
+
template: `# ${artifactId}`,
|
|
153
|
+
dependencies: [],
|
|
154
|
+
unlocks: [],
|
|
155
|
+
},
|
|
156
|
+
}),
|
|
136
157
|
instructionsApply: async (cwd: string, changeName: string) => ({
|
|
137
158
|
command: `openspec instructions apply --change ${changeName} --json`,
|
|
138
159
|
cwd,
|
|
@@ -133,7 +133,7 @@ test("apply reports no new planning notes when the chat context is detached", as
|
|
|
133
133
|
|
|
134
134
|
test("cs-plan runs visible planning sync and writes a fresh snapshot", async () => {
|
|
135
135
|
const harness = await createServiceHarness("clawspec-visible-plan-");
|
|
136
|
-
const { service, stateStore, repoPath } = harness;
|
|
136
|
+
const { service, stateStore, repoPath, openSpec } = harness;
|
|
137
137
|
const channelKey = "discord:visible-plan:default:main";
|
|
138
138
|
const promptContext = {
|
|
139
139
|
trigger: "user",
|
|
@@ -149,6 +149,13 @@ test("cs-plan runs visible planning sync and writes a fresh snapshot", async ()
|
|
|
149
149
|
await service.proposalProject(channelKey, "demo-change Demo change");
|
|
150
150
|
await service.recordPlanningMessageFromContext(promptContext, "add another API endpoint");
|
|
151
151
|
|
|
152
|
+
const instructionCalls: string[] = [];
|
|
153
|
+
const originalInstructionsArtifact = openSpec.instructionsArtifact;
|
|
154
|
+
openSpec.instructionsArtifact = async (...args: unknown[]) => {
|
|
155
|
+
instructionCalls.push(String(args[1]));
|
|
156
|
+
return await originalInstructionsArtifact(...args);
|
|
157
|
+
};
|
|
158
|
+
|
|
152
159
|
const injected = await service.handleBeforePromptBuild(
|
|
153
160
|
{ prompt: "cs-plan", messages: [] },
|
|
154
161
|
promptContext,
|
|
@@ -156,6 +163,11 @@ test("cs-plan runs visible planning sync and writes a fresh snapshot", async ()
|
|
|
156
163
|
const runningProject = await stateStore.getActiveProject(channelKey);
|
|
157
164
|
|
|
158
165
|
assert.match(injected?.prependContext ?? "", /ClawSpec planning sync is active for this turn/);
|
|
166
|
+
assert.match(injected?.prependContext ?? "", /Prefetched OpenSpec instructions for this turn/);
|
|
167
|
+
assert.match(injected?.prependContext ?? "", /OpenSpec commands already executed by the plugin before this turn/);
|
|
168
|
+
assert.match(injected?.prependContext ?? "", /openspec instructions proposal --change demo-change --json/);
|
|
169
|
+
assert.match(injected?.prependContext ?? "", /planning-instructions[\\/]+proposal\.json/);
|
|
170
|
+
assert.deepEqual(instructionCalls, ["proposal", "specs", "design", "tasks"]);
|
|
159
171
|
assert.match(injected?.prependContext ?? "", /mandatory final line exactly in this shape/i);
|
|
160
172
|
assert.equal(runningProject?.status, "planning");
|
|
161
173
|
assert.equal(runningProject?.phase, "planning_sync");
|