@gr8ful/spf 0.8.0 → 0.8.1

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.
@@ -316,6 +316,23 @@ export declare function refine(opts?: {
316
316
  retries?: number;
317
317
  extraGates?: string[];
318
318
  }): Step;
319
+ /**
320
+ * `cli/commands/watch.ts`'s `runRefine()` reads BOTH `refine_publish.json`
321
+ * and `refine_questions.json` back, unconditionally, after this chain
322
+ * exits — it has no other way to learn what THIS run did, since a chain's
323
+ * return value is just an exit code. A resumed spec (`continue-refinement`)
324
+ * reruns this entire chain from `request` on, into the SAME deterministic
325
+ * `context_handoff_dir` a PRIOR round already wrote into. Without clearing
326
+ * the file this run is NOT about to write, a stale `refine_questions.json`
327
+ * from an earlier escalation round survives a LATER round's successful
328
+ * publish — `runRefine` then reports those old questions as if raised
329
+ * again THIS round, so `runSpec` escalates a second time even though real
330
+ * issues were already created on the tracker seconds earlier. Called
331
+ * before EITHER branch writes, so exactly one of the two files reflects
332
+ * this run when the phase returns, never a leftover from a previous one.
333
+ * `force: true` — a first-ever run has neither file yet, which is fine.
334
+ */
335
+ export declare function clearStaleRefineOutputFiles(contextHandoffDir: string): void;
319
336
  export declare function publishIssues(opts?: {
320
337
  description?: string;
321
338
  }): Step;
@@ -40,7 +40,7 @@
40
40
  * chain is a load-time problem the loader can report against a file and a
41
41
  * line, never a phase that blows up ten minutes into an unattended run.
42
42
  */
43
- import { writeFileSync } from "node:fs";
43
+ import { rmSync, writeFileSync } from "node:fs";
44
44
  import path from "node:path";
45
45
  import * as changesLib from "../core/changes.js";
46
46
  import * as gates from "../core/gates.js";
@@ -723,6 +723,27 @@ function parsePriorityOption(raw) {
723
723
  }
724
724
  return raw;
725
725
  }
726
+ /**
727
+ * `cli/commands/watch.ts`'s `runRefine()` reads BOTH `refine_publish.json`
728
+ * and `refine_questions.json` back, unconditionally, after this chain
729
+ * exits — it has no other way to learn what THIS run did, since a chain's
730
+ * return value is just an exit code. A resumed spec (`continue-refinement`)
731
+ * reruns this entire chain from `request` on, into the SAME deterministic
732
+ * `context_handoff_dir` a PRIOR round already wrote into. Without clearing
733
+ * the file this run is NOT about to write, a stale `refine_questions.json`
734
+ * from an earlier escalation round survives a LATER round's successful
735
+ * publish — `runRefine` then reports those old questions as if raised
736
+ * again THIS round, so `runSpec` escalates a second time even though real
737
+ * issues were already created on the tracker seconds earlier. Called
738
+ * before EITHER branch writes, so exactly one of the two files reflects
739
+ * this run when the phase returns, never a leftover from a previous one.
740
+ * `force: true` — a first-ever run has neither file yet, which is fine.
741
+ */
742
+ export function clearStaleRefineOutputFiles(contextHandoffDir) {
743
+ for (const name of ["refine_questions.json", "refine_publish.json"]) {
744
+ rmSync(path.join(contextHandoffDir, name), { force: true });
745
+ }
746
+ }
726
747
  export function publishIssues(opts = {}) {
727
748
  preflightDescription("publish", opts.description);
728
749
  const fn = async (run, state) => {
@@ -738,6 +759,7 @@ export function publishIssues(opts = {}) {
738
759
  owner: "tracker",
739
760
  description: opts.description ?? "Create the feature/story tree on the tracker, in dependency order, and link each to its parent",
740
761
  }), async (ph) => {
762
+ clearStaleRefineOutputFiles(run.context_handoff_dir);
741
763
  if (questions.length > 0) {
742
764
  writeFileSync(path.join(run.context_handoff_dir, "refine_questions.json"), JSON.stringify(questions, null, 2));
743
765
  ph.log({ escalated: questions.length });
@@ -457,6 +457,18 @@ export async function watchCommand(argv) {
457
457
  catch {
458
458
  // best-effort, same as above — no questions file means this run wasn't an escalation
459
459
  }
460
+ // Defense in depth: `steps.publishIssues()` now clears whichever of
461
+ // these two files it's NOT about to write, so both should never be
462
+ // non-empty here — but if some future change (or an older worktree's
463
+ // leftover files, before that fix existed) ever produces both, a real
464
+ // completed publish must never be silently overridden by a stale
465
+ // question. `runSpec` checks `questions.length > 0` first, so without
466
+ // this it would re-escalate over issues that already landed on the
467
+ // tracker seconds earlier.
468
+ if (created.length > 0 && questions.length > 0) {
469
+ console.error(`watch: ${opts.adwId}: refine_publish.json AND refine_questions.json both had content — treating the ${created.length} published issue(s) as authoritative and discarding the stale question(s)`);
470
+ questions = [];
471
+ }
460
472
  return { accepted: true, adwId: opts.adwId, detail: "", created, questions };
461
473
  };
462
474
  // `IssueAuthoringProvider`'s read-back half — `isAuthoringProvider()` is a
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gr8ful/spf",
3
- "version": "0.8.0",
3
+ "version": "0.8.1",
4
4
  "description": "Super Portable Factory — a global CLI for repeatable agents-plus-code workflows (ADWs)",
5
5
  "type": "module",
6
6
  "license": "MIT",