clawspec 1.0.8 → 1.0.9

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.8",
3
+ "version": "1.0.9",
4
4
  "type": "module",
5
5
  "description": "OpenClaw plugin that orchestrates OpenSpec project workflows with visible main-agent execution.",
6
6
  "keywords": [
@@ -951,7 +951,8 @@ export class ClawSpecService {
951
951
  return;
952
952
  }
953
953
 
954
- const planningProject = await this.findPlanningProjectBySessionKey(ctx.sessionKey);
954
+ const planningProject = await this.findPlanningProjectBySessionKey(ctx.sessionKey)
955
+ ?? await this.findPlanningProjectByContext(ctx);
955
956
  if (planningProject) {
956
957
  await this.finalizePlanningTurn(planningProject, event);
957
958
  return;
@@ -2314,6 +2315,27 @@ export class ClawSpecService {
2314
2315
  );
2315
2316
  }
2316
2317
 
2318
+ private async findPlanningProjectByContext(ctx: PromptBuildContext): Promise<ProjectState | undefined> {
2319
+ const routingContext = deriveRoutingContext(ctx);
2320
+ if (!routingContext.channelId) {
2321
+ return undefined;
2322
+ }
2323
+
2324
+ const match = await this.stateStore.findActiveProjectForMessage({
2325
+ channel: routingContext.channel,
2326
+ channelId: routingContext.channelId,
2327
+ accountId: routingContext.accountId,
2328
+ conversationId: routingContext.conversationId,
2329
+ });
2330
+ if (!match) {
2331
+ return undefined;
2332
+ }
2333
+
2334
+ return match.project.status === "planning" && match.project.phase === "planning_sync"
2335
+ ? match.project
2336
+ : undefined;
2337
+ }
2338
+
2317
2339
  private async findDiscussionProjectBySessionKey(sessionKey?: string): Promise<ProjectState | undefined> {
2318
2340
  if (!sessionKey) {
2319
2341
  return undefined;
@@ -3172,6 +3194,10 @@ function isPassiveAssistantPlanningMessage(text: string): boolean {
3172
3194
  return true;
3173
3195
  }
3174
3196
 
3197
+ if (/^heartbeat(?:[_\s-]?(?:ok|ping|alive))?$/i.test(normalized)) {
3198
+ return true;
3199
+ }
3200
+
3175
3201
  const collapsed = normalized.replace(/\s+/g, " ").trim();
3176
3202
  if (/^[▶✓⚠↻ℹ]/u.test(collapsed)) {
3177
3203
  return true;
@@ -190,16 +190,18 @@ export function buildPlanningPrependContext(params: {
190
190
  "1. Read planning-journal.jsonl, .openspec.yaml, and any planning artifacts that already exist.",
191
191
  "2. Treat the prefetched OpenSpec instruction files in this context as the authoritative source for artifact structure, output paths, and writing guidance.",
192
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.",
196
- "7. Before updating each artifact, post a short chat update naming the artifact you are about to refresh.",
197
- "8. After updating each artifact, post a short chat update describing what changed and what artifact comes next.",
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.",
193
+ "4. The allowed planning scope is limited to what is explicitly supported by the current user message, the planning journal, existing dependency artifacts, and the prefetched OpenSpec instructions.",
194
+ "5. Do not invent endpoints, features, constraints, files, acceptance criteria, test scenarios, or architecture details that are not grounded in those sources.",
195
+ "6. If there is no substantive planning change, say so clearly in chat and do not rewrite artifacts unnecessarily.",
196
+ "7. If artifacts are missing or stale, update `proposal`, `specs`, `design`, and `tasks` in dependency order using those prefetched OpenSpec instruction files.",
197
+ "8. Do not generate or rewrite planning artifacts from ad-hoc structure guesses; follow OpenSpec instruction/template constraints only.",
198
+ "9. Before updating each artifact, post a short chat update naming the artifact you are about to refresh.",
199
+ "10. After updating each artifact, post a short chat update describing what changed and what artifact comes next.",
200
+ "11. For any implementation-oriented task item, ensure tasks.md contains an explicit testing task (new tests or updated tests) and a validation command.",
201
+ "12. Stop after planning artifacts are refreshed and apply-ready. Do not implement code in this turn.",
202
+ "13. End with a concise summary and a mandatory final line exactly in this shape: `Next: run `cs-work` to start implementation.`",
203
+ "14. Never scan sibling directories under `openspec/changes`, never switch to another change, and never restore or rewrite unrelated files.",
204
+ "15. Do not claim that OpenSpec instructions were skipped in this sync turn. The plugin already executed those commands before this turn began.",
203
205
  ]
204
206
  : [
205
207
  "Discussion rules for this turn:",
@@ -166,3 +166,38 @@ test("inbound bot/system messages are ignored by planning journal capture", asyn
166
166
  assert.equal(project?.planningJournal?.dirty, false);
167
167
  assert.equal(project?.planningJournal?.entryCount, 0);
168
168
  });
169
+
170
+ test("heartbeat assistant replies are not appended to the planning journal", async () => {
171
+ const harness = await createServiceHarness("clawspec-ignore-heartbeat-journal-");
172
+ const { service, repoPath } = harness;
173
+ const channelKey = "discord:ignore-heartbeat-journal:default:main";
174
+ const promptContext = createPromptContext("ignore-heartbeat-journal");
175
+ const userPrompt = "Add one more API endpoint.";
176
+
177
+ await service.startProject(channelKey);
178
+ await service.useProject(channelKey, "demo-app");
179
+ await service.proposalProject(channelKey, "demo-change Demo change");
180
+ await service.recordPlanningMessageFromContext(promptContext, userPrompt);
181
+ await service.handleBeforePromptBuild(
182
+ { prompt: userPrompt, messages: [] },
183
+ promptContext,
184
+ );
185
+
186
+ await service.handleAgentEnd(
187
+ {
188
+ success: true,
189
+ messages: [
190
+ { role: "user", content: userPrompt },
191
+ { role: "assistant", content: "HEARTBEAT_OK" },
192
+ ],
193
+ },
194
+ promptContext,
195
+ );
196
+
197
+ const repoStatePaths = getRepoStatePaths(repoPath, "archives");
198
+ const journalStore = new PlanningJournalStore(repoStatePaths.planningJournalFile);
199
+ const entries = await journalStore.list("demo-change");
200
+
201
+ assert.equal(entries.length, 1);
202
+ assert.equal(entries[0]?.role, "user");
203
+ });
@@ -168,6 +168,8 @@ test("cs-plan runs visible planning sync and writes a fresh snapshot", async ()
168
168
  assert.match(injected?.prependContext ?? "", /openspec instructions proposal --change demo-change --json/);
169
169
  assert.match(injected?.prependContext ?? "", /planning-instructions[\\/]+proposal\.json/);
170
170
  assert.deepEqual(instructionCalls, ["proposal", "specs", "design", "tasks"]);
171
+ assert.match(injected?.prependContext ?? "", /allowed planning scope is limited/i);
172
+ assert.match(injected?.prependContext ?? "", /Do not invent endpoints, features, constraints, files, acceptance criteria, test scenarios, or architecture details/i);
171
173
  assert.match(injected?.prependContext ?? "", /mandatory final line exactly in this shape/i);
172
174
  assert.equal(runningProject?.status, "planning");
173
175
  assert.equal(runningProject?.phase, "planning_sync");
@@ -207,6 +209,48 @@ test("cs-plan runs visible planning sync and writes a fresh snapshot", async ()
207
209
  assert.equal(snapshot?.changeName, "demo-change");
208
210
  });
209
211
 
212
+ test("cs-plan finalizes and writes a fresh snapshot even if agent_end sessionKey changes", async () => {
213
+ const harness = await createServiceHarness("clawspec-visible-plan-fallback-");
214
+ const { service, stateStore, repoPath } = harness;
215
+ const channelKey = "discord:visible-plan-fallback:default:main";
216
+ const promptContext = {
217
+ trigger: "user",
218
+ channel: "discord",
219
+ channelId: "visible-plan-fallback",
220
+ accountId: "default",
221
+ conversationId: "main",
222
+ sessionKey: "agent:main:discord:channel:visible-plan-fallback",
223
+ };
224
+
225
+ await service.startProject(channelKey);
226
+ await service.useProject(channelKey, "demo-app");
227
+ await service.proposalProject(channelKey, "demo-change Demo change");
228
+ await service.recordPlanningMessageFromContext(promptContext, "add another API endpoint");
229
+
230
+ await service.handleBeforePromptBuild(
231
+ { prompt: "cs-plan", messages: [] },
232
+ promptContext,
233
+ );
234
+
235
+ await service.handleAgentEnd(
236
+ { messages: [], success: true, durationMs: 10 },
237
+ {
238
+ ...promptContext,
239
+ sessionKey: "agent:main:discord:channel:visible-plan-fallback:other",
240
+ },
241
+ );
242
+
243
+ const finalized = await stateStore.getActiveProject(channelKey);
244
+ const repoStatePaths = getRepoStatePaths(repoPath, "archives");
245
+ const snapshot = await readJsonFile<any>(repoStatePaths.planningJournalSnapshotFile, null);
246
+
247
+ assert.equal(finalized?.status, "ready");
248
+ assert.equal(finalized?.phase, "tasks");
249
+ assert.equal(finalized?.planningJournal?.dirty, false);
250
+ assert.equal(snapshot?.changeName, "demo-change");
251
+ assert.equal(snapshot?.entryCount, 1);
252
+ });
253
+
210
254
  test("cs-plan clears stale execution control artifacts from earlier worker runs", async () => {
211
255
  const harness = await createServiceHarness("clawspec-visible-plan-cleanup-");
212
256
  const { service, stateStore, repoPath } = harness;