@gobing-ai/spur 0.3.88 → 0.3.90

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.
Files changed (35) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/config/rules/boundary/env-var-hygiene.yaml +13 -5
  3. package/config/rules/boundary/sp-plugin-standalone.yaml +37 -0
  4. package/package.json +9 -9
  5. package/plugins/sp/hooks/agent-hint.ts +1 -1
  6. package/plugins/sp/hooks/careful-guard.ts +1 -1
  7. package/plugins/sp/hooks/context-post-tool.ts +1 -1
  8. package/plugins/sp/hooks/context-session-start.ts +1 -1
  9. package/plugins/sp/hooks/context-session-stop.ts +1 -1
  10. package/plugins/sp/hooks/pi/guard-extension.ts +1 -1
  11. package/plugins/sp/hooks/task-write-guard.ts +1 -1
  12. package/plugins/sp/lib/env.ts +46 -0
  13. package/plugins/sp/lib/idea-handoff.generated.mjs +157 -150
  14. package/plugins/sp/plugin.json +1 -1
  15. package/plugins/sp/scripts/daily-summary/daily-summary.mjs +1 -1
  16. package/plugins/sp/scripts/daily-summary/daily-summary.ts +1 -1
  17. package/plugins/sp/scripts/feature-sync-bounded.mjs +1 -1
  18. package/plugins/sp/scripts/feature-sync-bounded.ts +1 -1
  19. package/plugins/sp/scripts/idea-handoff.mjs +1 -1
  20. package/plugins/sp/scripts/idea-handoff.ts +1 -1
  21. package/plugins/sp/scripts/inline-run-setup.ts +1 -1
  22. package/plugins/sp/scripts/pr-reviewing.mjs +1 -1
  23. package/plugins/sp/scripts/pr-reviewing.ts +1 -1
  24. package/plugins/sp/scripts/quality-gate.mjs +1 -1
  25. package/plugins/sp/scripts/quality-gate.ts +1 -1
  26. package/plugins/sp/scripts/script-contract-check.ts +58 -0
  27. package/plugins/sp/scripts/task-evidence-precheck.ts +1 -1
  28. package/plugins/sp/scripts/task-size-precheck.ts +1 -1
  29. package/plugins/sp/scripts/verify-answer-lint.ts +1 -1
  30. package/plugins/sp/scripts/workflow-step-profile.mjs +1 -1
  31. package/plugins/sp/scripts/workflow-step-profile.ts +1 -1
  32. package/plugins/sp/scripts/wrapup-steps.mjs +1 -1
  33. package/plugins/sp/scripts/wrapup-steps.ts +1 -1
  34. package/plugins/sp/skills/spur-cli/references/workflows.md +8 -3
  35. package/spur.js +541 -246
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sp",
3
- "version": "0.3.88",
3
+ "version": "0.3.90",
4
4
  "description": "Spur — a local-first harness engineering toolkit that wraps mainstream coding agents with constraint checking, workflow orchestration, and history analytics.",
5
5
  "extensions": {
6
6
  "pi": ["./hooks/pi/guard-extension.ts"]
@@ -8,7 +8,7 @@ import { spawn, spawnSync } from "child_process";
8
8
  import { existsSync, mkdirSync, readlinkSync, writeFileSync } from "fs";
9
9
  import { dirname, isAbsolute, join, resolve } from "path";
10
10
 
11
- // ../ts-libs/packages/utils/dist/env.js
11
+ // plugins/sp/lib/env.ts
12
12
  function getEnvVar(name, fallback) {
13
13
  const raw = process.env[name];
14
14
  return raw === undefined ? fallback : raw;
@@ -11,7 +11,7 @@
11
11
  import { spawn, spawnSync } from 'node:child_process';
12
12
  import { existsSync, mkdirSync, readlinkSync, writeFileSync } from 'node:fs';
13
13
  import { dirname, isAbsolute, join, resolve } from 'node:path';
14
- import { getEnvVar, getEnvVars } from '@gobing-ai/ts-utils';
14
+ import { getEnvVar, getEnvVars } from '../../lib/env';
15
15
 
16
16
  import { logger } from './logger';
17
17
 
@@ -9,7 +9,7 @@ import { existsSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSy
9
9
  import { dirname } from "node:path";
10
10
  import { fileURLToPath } from "node:url";
11
11
 
12
- // ../ts-libs/packages/utils/dist/env.js
12
+ // plugins/sp/lib/env.ts
13
13
  function getEnvVar(name, fallback) {
14
14
  const raw = process.env[name];
15
15
  return raw === undefined ? fallback : raw;
@@ -24,7 +24,7 @@ import { createHash } from 'node:crypto';
24
24
  import { existsSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from 'node:fs';
25
25
  import { dirname } from 'node:path';
26
26
  import { fileURLToPath } from 'node:url';
27
- import { getEnvVar } from '@gobing-ai/ts-utils';
27
+ import { getEnvVar } from '../lib/env';
28
28
 
29
29
  // ── Local types (match packages/app FeatureService shapes; no package import) ───────────
30
30
 
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  // @bun
3
3
 
4
- // ../ts-libs/packages/utils/dist/env.js
4
+ // plugins/sp/lib/env.ts
5
5
  function getEnvVars() {
6
6
  return process.env;
7
7
  }
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env bun
2
2
 
3
- import { getEnvVars } from '@gobing-ai/ts-utils';
3
+ import { getEnvVars } from '../lib/env';
4
4
 
5
5
  /**
6
6
  * idea-handoff — portable entrypoint for the idea-pipeline finalization (task 0824, feature I21).
@@ -52,7 +52,7 @@
52
52
  import { appendFileSync, existsSync, mkdirSync, writeFileSync } from 'node:fs';
53
53
  import { dirname, join, resolve } from 'node:path';
54
54
  import { fileURLToPath } from 'node:url';
55
- import { getEnvVar } from '@gobing-ai/ts-utils';
55
+ import { getEnvVar } from '../lib/env';
56
56
 
57
57
  /** Outcome document written to `.spur/run/<run-id>-inline-setup.json`. */
58
58
  interface SetupOutcome {
@@ -6,7 +6,7 @@ import { spawnSync } from "child_process";
6
6
  import { existsSync, readFileSync, writeFileSync } from "fs";
7
7
  import { join } from "path";
8
8
 
9
- // ../ts-libs/packages/utils/dist/env.js
9
+ // plugins/sp/lib/env.ts
10
10
  function getEnvVars() {
11
11
  return process.env;
12
12
  }
@@ -22,7 +22,7 @@
22
22
  import { spawnSync } from 'node:child_process';
23
23
  import { existsSync, readFileSync, writeFileSync } from 'node:fs';
24
24
  import { join } from 'node:path';
25
- import { getEnvVars } from '@gobing-ai/ts-utils';
25
+ import { getEnvVars } from '../lib/env';
26
26
 
27
27
  // ─── Types ──────────────────────────────────────────────────────────────────
28
28
 
@@ -18,7 +18,7 @@ import {
18
18
  import { tmpdir } from "os";
19
19
  import { join } from "path";
20
20
 
21
- // ../ts-libs/packages/utils/dist/env.js
21
+ // plugins/sp/lib/env.ts
22
22
  function getEnvVars() {
23
23
  return process.env;
24
24
  }
@@ -41,7 +41,7 @@ import {
41
41
  } from 'node:fs';
42
42
  import { tmpdir } from 'node:os';
43
43
  import { join } from 'node:path';
44
- import { getEnvVars } from '@gobing-ai/ts-utils';
44
+ import { getEnvVars } from '../lib/env';
45
45
 
46
46
  export const MAX_GATE_ATTEMPTS = 5;
47
47
  export const MAX_FINDINGS = 20;
@@ -178,6 +178,47 @@ export function scanShippedSurfaces(pluginDir: string): Array<{ file: string; li
178
178
  return matches;
179
179
  }
180
180
 
181
+ // Bare-specifier value imports in bundled surfaces break `superskill install`, which
182
+ // bundles hooks/scripts on targets with no node_modules (task 0669; releases
183
+ // 0.3.81–0.3.88 failed with "Bundle failed" when scripts imported @gobing-ai/ts-utils).
184
+ // Type-only imports are erased before bundling, so they stay legal. Line-based
185
+ // statement walk (import/export statements are column 0 under biome) — lookahead
186
+ // regexes misbehave in Bun 1.3.x (JSC) when grouped with lazy quantifiers.
187
+ function findGobingAiValueImports(dir: string): { file: string; line: number }[] {
188
+ const hits: { file: string; line: number }[] = [];
189
+ let entries: string[];
190
+ try {
191
+ entries = readdirSync(dir);
192
+ } catch {
193
+ return hits;
194
+ }
195
+ for (const entry of entries) {
196
+ const full = join(dir, entry);
197
+ let st: ReturnType<typeof statSync>;
198
+ try {
199
+ st = statSync(full);
200
+ } catch {
201
+ continue;
202
+ }
203
+ if (st.isDirectory()) {
204
+ hits.push(...findGobingAiValueImports(full));
205
+ } else if (entry.endsWith('.ts')) {
206
+ const lines = readFileSync(full, 'utf8').split('\n');
207
+ let stmtStart = 0;
208
+ for (let i = 0; i < lines.length; i++) {
209
+ if (/^\s*(?:import|export)\b/.test(lines[i])) stmtStart = i;
210
+ if (/(?:from\s*|import\s*|import\s*\(\s*)['"]@gobing-ai\//.test(lines[i])) {
211
+ const opener = lines.slice(stmtStart, i + 1).join(' ');
212
+ if (!/^\s*(?:import|export)\s+type\b/.test(opener)) {
213
+ hits.push({ file: full, line: i + 1 });
214
+ }
215
+ }
216
+ }
217
+ }
218
+ }
219
+ return hits;
220
+ }
221
+
181
222
  export function validateContract(manifest: ScriptManifest, scriptsDir: string, pluginDir: string): Violation[] {
182
223
  const violations: Violation[] = [];
183
224
  const { tsFiles, mjsFiles } = listDiskScripts(scriptsDir);
@@ -280,6 +321,23 @@ export function validateContract(manifest: ScriptManifest, scriptsDir: string, p
280
321
  });
281
322
  }
282
323
 
324
+ // Rule 5: bundled surfaces (scripts, hooks, lib) must not value-import @gobing-ai/*.
325
+ // superskill install bundles them where no node_modules exists; resolution then
326
+ // depends on the host superskill's private dep tree (0.3.28+ happens to carry
327
+ // ts-utils — an accident we do not depend on). Vendor into plugins/sp/lib/ instead.
328
+ for (const dir of [scriptsDir, join(pluginDir, 'hooks'), join(pluginDir, 'lib')]) {
329
+ for (const hit of findGobingAiValueImports(dir)) {
330
+ violations.push({
331
+ kind: 'gobing_ai_import',
332
+ target: `${hit.file}:${hit.line}`,
333
+ message:
334
+ `bundled surface ${hit.file}:${hit.line} value-imports @gobing-ai/* — ` +
335
+ 'superskill install bundles it with no node_modules ("Bundle failed", task 0669). ' +
336
+ 'Vendor into plugins/sp/lib/ or use a relative import; `import type` is exempt.',
337
+ });
338
+ }
339
+ }
340
+
283
341
  return violations;
284
342
  }
285
343
 
@@ -37,7 +37,7 @@ import { execFileSync } from 'node:child_process';
37
37
  import { existsSync, mkdirSync, writeFileSync } from 'node:fs';
38
38
  import { join } from 'node:path';
39
39
  import { fileURLToPath } from 'node:url';
40
- import { getEnvVar } from '@gobing-ai/ts-utils';
40
+ import { getEnvVar } from '../lib/env';
41
41
 
42
42
  /** Exact task-content declaration that activates the live-evidence gate (0726 R2). */
43
43
  const DECLARATION_PREFIX = 'evidence-channel:';
@@ -26,7 +26,7 @@ import { execFileSync } from 'node:child_process';
26
26
  import { existsSync, mkdirSync, writeFileSync } from 'node:fs';
27
27
  import { join } from 'node:path';
28
28
  import { fileURLToPath } from 'node:url';
29
- import { getEnvVar } from '@gobing-ai/ts-utils';
29
+ import { getEnvVar } from '../lib/env';
30
30
 
31
31
  // ─── Counting (sync with packages/app/src/services/task-size-precheck.ts) ────
32
32
 
@@ -32,7 +32,7 @@
32
32
  import { execFileSync } from 'node:child_process';
33
33
  import { existsSync, readFileSync } from 'node:fs';
34
34
  import { fileURLToPath } from 'node:url';
35
- import { getEnvVar } from '@gobing-ai/ts-utils';
35
+ import { getEnvVar } from '../lib/env';
36
36
 
37
37
  // ─── CLI (same spur-bin chain as task-evidence-precheck.ts) ─────────────────
38
38
 
@@ -6,7 +6,7 @@ import { spawnSync } from "child_process";
6
6
  import { existsSync } from "fs";
7
7
  import { fileURLToPath } from "url";
8
8
 
9
- // ../ts-libs/packages/utils/dist/env.js
9
+ // plugins/sp/lib/env.ts
10
10
  function getEnvVar(name, fallback) {
11
11
  const raw = process.env[name];
12
12
  return raw === undefined ? fallback : raw;
@@ -25,7 +25,7 @@
25
25
  import { spawnSync } from 'node:child_process';
26
26
  import { existsSync } from 'node:fs';
27
27
  import { fileURLToPath } from 'node:url';
28
- import { getEnvVar } from '@gobing-ai/ts-utils';
28
+ import { getEnvVar } from '../lib/env';
29
29
 
30
30
  // ── Input shapes (a subset of the live `spur workflow trace --json` payload) ─────────────
31
31
 
@@ -6,7 +6,7 @@ import { spawnSync } from "child_process";
6
6
  import { appendFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
7
7
  import { join } from "path";
8
8
 
9
- // ../ts-libs/packages/utils/dist/env.js
9
+ // plugins/sp/lib/env.ts
10
10
  function getEnvVars() {
11
11
  return process.env;
12
12
  }
@@ -31,7 +31,7 @@
31
31
  import { spawnSync } from 'node:child_process';
32
32
  import { appendFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
33
33
  import { join } from 'node:path';
34
- import { getEnvVars } from '@gobing-ai/ts-utils';
34
+ import { getEnvVars } from '../lib/env';
35
35
 
36
36
  export interface WrapupStepsEnv {
37
37
  __runId?: string;
@@ -108,7 +108,7 @@ The skill's logic divides by **whether the LLM adds value**:
108
108
  | --------- | --------- | ----- | ------------------ |
109
109
  | `validate` | `spur workflow validate` (CLI) | `<file> [--no-schema]` | Schema + semantic verdict |
110
110
  | `run` | `spur workflow run` (CLI) | `<file> [--run-id <id>] [--vars <json>] [--dry-run] [--async] [--no-plan] [--quiet/--silent/--verbose] [--detail <level>] [--trace-file] [--no-log] [--steer]` | Terminal state reached (sync) or run started (async); trace readable |
111
- | `continue` | `spur workflow continue` (CLI) | `[run-id] [--yes] [--answer <yes\|no\|cancel>]` | Resume a paused HITL run (omit id -> most recent paused); `--answer` injects a gate answer before guard re-evaluation |
111
+ | `continue` | `spur workflow continue` (CLI) | `[run-id] [--yes] [--answer <yes\|no\|cancel>] [--async] [--no-log]` | Resume a paused or interrupted run (omit id -> most recent resumable); `--answer` injects a gate answer before guard re-evaluation and is required headless (0901 R3); `--async` detaches the resume (0901 R4) |
112
112
  | `cancel` | `spur workflow cancel` (CLI) | `<run-id>` | Single non-terminal run marked failed (SIGTERM async worker when live) |
113
113
  | `clean` | `spur workflow clean` (CLI) | `[--older-than <min>] [--force] [--logs] [--dry-run]` | Bulk-finalize stale `running`/`pending` runs as failed **and** reclaim retained run logs older than `workflow.logRetentionDays` (30d default) |
114
114
  | `list` | `spur workflow list` (CLI) | — | Available workflow **YAML definition files** (not run records) |
@@ -258,7 +258,7 @@ the operator accepts it, and never hot-edit a running workflow's shell in place.
258
258
  spur workflow validate <file> [--no-schema] [--json]
259
259
  spur workflow show <file> [--format <mermaid|todo>] [--json]
260
260
  spur workflow run <file> [--run-id <id>] [--vars <json>] [--dry-run] [--async] [--no-plan] [--quiet/--silent/--verbose] [--detail <level>] [--trace-file] [--no-log] [--steer] [--json]
261
- spur workflow continue [run-id] [--yes] [--answer <yes|no|cancel>] [--json]
261
+ spur workflow continue [run-id] [--yes] [--answer <yes|no|cancel>] [--async] [--no-log] [--json]
262
262
  spur workflow cancel <run-id> [--json]
263
263
  spur workflow clean [--older-than <minutes>] [--force] [--logs] [--dry-run] [--json]
264
264
  spur workflow list [--json]
@@ -318,7 +318,12 @@ HITL pause/resume: a run that hits a HITL action pauses; resume with `spur workf
318
318
  (`--yes` skips confirmation). A headless `hitl.confirm` persists a default `no` before pausing -
319
319
  use `--answer yes|no|cancel` to inject the operator's gate answer before guard re-evaluation (0433).
320
320
  `--answer` is distinct from `--yes`: `--yes` skips the CLI resume prompt, `--answer` sets the HITL
321
- gate answer. Cancel one live/paused run with `cancel <run-id>`; bulk-finalize orphans stuck in
321
+ gate answer. 0901: resumes accept interrupted runs too (rerun-enter re-executes the interrupted
322
+ node and needs `resumeRerun: true` on the target state); a headless (`--json`/non-TTY) continue
323
+ without `--answer` is refused (exit 2); `--async` detaches the resume and reports started/failed
324
+ after the worker claims the run; resumed runs write the consolidated run log and pass shell
325
+ streams through the secret redactor + 64 KiB tail unless `--no-log`. Cancel one live/paused run
326
+ with `cancel <run-id>`; bulk-finalize orphans stuck in
322
327
  `running`/`pending` with `clean` (`--older-than` default 30 minutes, or `--force`).
323
328
 
324
329
  **Schema resolution parity (0431):** `validate` and `run` both load the workflow through