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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clawspec",
3
- "version": "1.0.5",
3
+ "version": "1.0.6",
4
4
  "type": "module",
5
5
  "description": "OpenClaw plugin that orchestrates OpenSpec project workflows with visible main-agent execution.",
6
6
  "keywords": [
@@ -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
- | { project: ProjectState; outputs: OpenSpecCommandResult[]; repoStatePaths: RepoStatePaths }
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 {
@@ -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),
@@ -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. 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.",
189
- "3. If there is no substantive planning change, say so clearly in chat and do not rewrite artifacts unnecessarily.",
190
- "4. Run `openspec status --change <name> --json` to inspect artifact readiness.",
191
- "5. If artifacts are missing or stale, use `openspec instructions <artifact> --change <name> --json` and update `proposal`, `specs`, `design`, and `tasks` in dependency order.",
192
- "6. Keep OpenSpec command activity visible by running those commands normally in this chat.",
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. Stop after planning artifacts are refreshed and apply-ready. Do not implement code in this turn.",
196
- "10. End with a concise summary and a mandatory final line exactly in this shape: `Next: run `cs-work` to start implementation.`",
197
- "11. Never scan sibling directories under `openspec/changes`, never switch to another change, and never restore or rewrite unrelated files.",
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
  "",
@@ -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");