clawspec 1.0.5 → 1.0.6
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 +63 -11
- package/src/utils/paths.ts +2 -0
- package/src/worker/prompts.ts +19 -8
- package/test/helpers/harness.ts +21 -0
- package/test/queue-planning.test.ts +2 -0
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,19 @@ 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),
|
|
587
590
|
}),
|
|
588
591
|
};
|
|
589
592
|
}
|
|
590
593
|
|
|
591
594
|
private async preparePlanningSync(channelKey: string): Promise<
|
|
592
595
|
| { result: PluginCommandResult }
|
|
593
|
-
| {
|
|
596
|
+
| {
|
|
597
|
+
project: ProjectState;
|
|
598
|
+
outputs: OpenSpecCommandResult[];
|
|
599
|
+
repoStatePaths: RepoStatePaths;
|
|
600
|
+
instructionResults: Array<OpenSpecCommandResult<OpenSpecInstructionsResponse>>;
|
|
601
|
+
}
|
|
594
602
|
> {
|
|
595
603
|
const project = await this.requireActiveProject(channelKey);
|
|
596
604
|
if (!project.repoPath || !project.projectName || !project.changeName) {
|
|
@@ -661,6 +669,21 @@ export class ClawSpecService {
|
|
|
661
669
|
}
|
|
662
670
|
const statusResult = await this.openSpec.status(project.repoPath, project.changeName);
|
|
663
671
|
outputs.push(statusResult);
|
|
672
|
+
const instructionResults = await this.refreshPlanningInstructionFiles(project, repoStatePaths);
|
|
673
|
+
outputs.push(...instructionResults);
|
|
674
|
+
await this.writeLatestSummary(
|
|
675
|
+
repoStatePaths,
|
|
676
|
+
`Planning instructions refreshed for ${project.changeName} via OpenSpec CLI.`,
|
|
677
|
+
);
|
|
678
|
+
await removeIfExists(repoStatePaths.executionControlFile);
|
|
679
|
+
await removeIfExists(repoStatePaths.executionResultFile);
|
|
680
|
+
await removeIfExists(repoStatePaths.workerProgressFile);
|
|
681
|
+
return {
|
|
682
|
+
project,
|
|
683
|
+
outputs,
|
|
684
|
+
repoStatePaths,
|
|
685
|
+
instructionResults,
|
|
686
|
+
};
|
|
664
687
|
} catch (error) {
|
|
665
688
|
if (error instanceof OpenSpecCommandError) {
|
|
666
689
|
return {
|
|
@@ -677,15 +700,6 @@ export class ClawSpecService {
|
|
|
677
700
|
}
|
|
678
701
|
throw error;
|
|
679
702
|
}
|
|
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
703
|
}
|
|
690
704
|
|
|
691
705
|
private async startVisiblePlanningSync(
|
|
@@ -717,7 +731,34 @@ export class ClawSpecService {
|
|
|
717
731
|
lastExecutionAt: startedAt,
|
|
718
732
|
}));
|
|
719
733
|
|
|
720
|
-
return await this.buildPlanningSyncInjection(runningProject, userPrompt);
|
|
734
|
+
return await this.buildPlanningSyncInjection(runningProject, userPrompt, prepared.instructionResults);
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
private async refreshPlanningInstructionFiles(
|
|
738
|
+
project: ProjectState,
|
|
739
|
+
repoStatePaths: RepoStatePaths,
|
|
740
|
+
): Promise<Array<OpenSpecCommandResult<OpenSpecInstructionsResponse>>> {
|
|
741
|
+
if (!project.repoPath || !project.changeName) {
|
|
742
|
+
return [];
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
const artifactIds = ["proposal", "specs", "design", "tasks"] as const;
|
|
746
|
+
await ensureDir(repoStatePaths.planningInstructionsRoot);
|
|
747
|
+
const results: Array<OpenSpecCommandResult<OpenSpecInstructionsResponse>> = [];
|
|
748
|
+
|
|
749
|
+
for (const artifactId of artifactIds) {
|
|
750
|
+
const result = await this.openSpec.instructionsArtifact(project.repoPath, artifactId, project.changeName);
|
|
751
|
+
results.push(result);
|
|
752
|
+
await writeJsonFile(path.join(repoStatePaths.planningInstructionsRoot, `${artifactId}.json`), {
|
|
753
|
+
generatedAt: new Date().toISOString(),
|
|
754
|
+
command: result.command,
|
|
755
|
+
cwd: result.cwd,
|
|
756
|
+
durationMs: result.durationMs,
|
|
757
|
+
instruction: result.parsed,
|
|
758
|
+
});
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
return results;
|
|
721
762
|
}
|
|
722
763
|
|
|
723
764
|
private async collectPlanningContextPaths(
|
|
@@ -728,6 +769,17 @@ export class ClawSpecService {
|
|
|
728
769
|
repoStatePaths.stateFile,
|
|
729
770
|
repoStatePaths.planningJournalFile,
|
|
730
771
|
];
|
|
772
|
+
const instructionFiles = [
|
|
773
|
+
path.join(repoStatePaths.planningInstructionsRoot, "proposal.json"),
|
|
774
|
+
path.join(repoStatePaths.planningInstructionsRoot, "specs.json"),
|
|
775
|
+
path.join(repoStatePaths.planningInstructionsRoot, "design.json"),
|
|
776
|
+
path.join(repoStatePaths.planningInstructionsRoot, "tasks.json"),
|
|
777
|
+
];
|
|
778
|
+
for (const instructionFile of instructionFiles) {
|
|
779
|
+
if (await pathExists(instructionFile)) {
|
|
780
|
+
paths.push(instructionFile);
|
|
781
|
+
}
|
|
782
|
+
}
|
|
731
783
|
|
|
732
784
|
if (!project.changeDir) {
|
|
733
785
|
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,7 @@ export function buildPlanningPrependContext(params: {
|
|
|
177
178
|
scaffoldOnly?: boolean;
|
|
178
179
|
mode: "discussion" | "sync";
|
|
179
180
|
nextActionHint?: "plan" | "work";
|
|
181
|
+
prefetchedInstructions?: OpenSpecInstructionsResponse[];
|
|
180
182
|
}): string {
|
|
181
183
|
const project = params.project;
|
|
182
184
|
|
|
@@ -185,16 +187,17 @@ export function buildPlanningPrependContext(params: {
|
|
|
185
187
|
"Required workflow for this turn:",
|
|
186
188
|
"0. The active change directory shown above is the only OpenSpec change directory you may inspect or modify in this turn.",
|
|
187
189
|
"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.
|
|
190
|
+
"2. Treat the prefetched OpenSpec instruction files in this context as the authoritative source for artifact structure, output paths, and writing guidance.",
|
|
191
|
+
"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.",
|
|
192
|
+
"4. If there is no substantive planning change, say so clearly in chat and do not rewrite artifacts unnecessarily.",
|
|
193
|
+
"5. If artifacts are missing or stale, update `proposal`, `specs`, `design`, and `tasks` in dependency order using those prefetched OpenSpec instruction files.",
|
|
194
|
+
"6. Do not generate or rewrite planning artifacts from ad-hoc structure guesses; follow OpenSpec instruction/template constraints only.",
|
|
193
195
|
"7. Before updating each artifact, post a short chat update naming the artifact you are about to refresh.",
|
|
194
196
|
"8. After updating each artifact, post a short chat update describing what changed and what artifact comes next.",
|
|
195
|
-
"9.
|
|
196
|
-
"10.
|
|
197
|
-
"11.
|
|
197
|
+
"9. For any implementation-oriented task item, ensure tasks.md contains an explicit testing task (new tests or updated tests) and a validation command.",
|
|
198
|
+
"10. Stop after planning artifacts are refreshed and apply-ready. Do not implement code in this turn.",
|
|
199
|
+
"11. End with a concise summary and a mandatory final line exactly in this shape: `Next: run `cs-work` to start implementation.`",
|
|
200
|
+
"12. Never scan sibling directories under `openspec/changes`, never switch to another change, and never restore or rewrite unrelated files.",
|
|
198
201
|
]
|
|
199
202
|
: [
|
|
200
203
|
"Discussion rules for this turn:",
|
|
@@ -232,6 +235,14 @@ export function buildPlanningPrependContext(params: {
|
|
|
232
235
|
"",
|
|
233
236
|
"Read these files before responding:",
|
|
234
237
|
...params.contextPaths.map((contextPath) => `- ${contextPath}`),
|
|
238
|
+
params.mode === "sync" ? "" : "",
|
|
239
|
+
params.mode === "sync" ? "Prefetched OpenSpec instructions for this turn:" : "",
|
|
240
|
+
...(params.mode === "sync"
|
|
241
|
+
? (params.prefetchedInstructions ?? [])
|
|
242
|
+
.map((instruction) =>
|
|
243
|
+
`- ${instruction.artifactId}: ${displayPath(resolveProjectScopedPath(project, instruction.outputPath))}`,
|
|
244
|
+
)
|
|
245
|
+
: []),
|
|
235
246
|
params.scaffoldOnly ? "" : "",
|
|
236
247
|
params.scaffoldOnly ? "Only the change scaffold exists right now. That is expected before planning sync generates the first artifacts." : "",
|
|
237
248
|
"",
|
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,
|
|
@@ -156,6 +156,8 @@ test("cs-plan runs visible planning sync and writes a fresh snapshot", async ()
|
|
|
156
156
|
const runningProject = await stateStore.getActiveProject(channelKey);
|
|
157
157
|
|
|
158
158
|
assert.match(injected?.prependContext ?? "", /ClawSpec planning sync is active for this turn/);
|
|
159
|
+
assert.match(injected?.prependContext ?? "", /Prefetched OpenSpec instructions for this turn/);
|
|
160
|
+
assert.match(injected?.prependContext ?? "", /planning-instructions[\\/]+proposal\.json/);
|
|
159
161
|
assert.match(injected?.prependContext ?? "", /mandatory final line exactly in this shape/i);
|
|
160
162
|
assert.equal(runningProject?.status, "planning");
|
|
161
163
|
assert.equal(runningProject?.phase, "planning_sync");
|