@gobing-ai/spur 0.3.99 → 0.4.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.
- package/.claude-plugin/marketplace.json +1 -1
- package/config/config.global.yaml +17 -0
- package/config/decisions/gates.yaml +36 -0
- package/config/decisions/history-anatomy.yaml +35 -0
- package/config/decisions/idea-pipeline.yaml +38 -0
- package/config/decisions/task-pipeline.yaml +55 -0
- package/config/plugin-scripts.json +25 -0
- package/config/rules/boundary/sp-runtime-path.yaml +2 -0
- package/config/rules/docs/sourced-api-claims.yaml +60 -0
- package/config/rules/quality/coverage-gate.yaml +5 -3
- package/config/rules/recommended-pre-check.yaml +8 -3
- package/config/rules/strict/runtime-boundaries.yaml +11 -0
- package/config/rules/structure/protected-files.yaml +3 -0
- package/config/rules/structure/scratch-evidence-pointer.yaml +35 -0
- package/config/script-placement-baseline.json +2 -2
- package/config/workflows/feature-lifecycle.yaml +9 -2
- package/config/workflows/history-anatomy.yaml +22 -0
- package/config/workflows/idea-pipeline.yaml +75 -0
- package/config/workflows/task-pipeline.yaml +368 -49
- package/config/workflows/wrapup-pipeline.yaml +106 -13
- package/package.json +9 -9
- package/plugins/sp/README.md +25 -2
- package/plugins/sp/agents/super-planner.md +11 -0
- package/plugins/sp/agents/super-reviewer.md +39 -11
- package/plugins/sp/bunfig.toml +8 -0
- package/plugins/sp/commands/dev-brainstorm.md +29 -16
- package/plugins/sp/commands/dev-changelog.md +5 -4
- package/plugins/sp/commands/dev-find-way.md +80 -0
- package/plugins/sp/commands/dev-idea.md +6 -0
- package/plugins/sp/commands/dev-parallel.md +8 -2
- package/plugins/sp/commands/dev-plan.md +6 -0
- package/plugins/sp/commands/dev-review-session.md +14 -7
- package/plugins/sp/commands/dev-run.md +17 -4
- package/plugins/sp/commands/dev-runall.md +21 -10
- package/plugins/sp/hooks/agent-lifecycle.ts +155 -0
- package/plugins/sp/hooks/context-session-start.ts +1 -1
- package/plugins/sp/hooks/fleet-guest-stop.ts +180 -0
- package/plugins/sp/hooks/hooks.json +45 -1
- package/plugins/sp/hooks/pi/guard-extension.ts +21 -0
- package/plugins/sp/lib/history-anatomy.generated.mjs +5 -0
- package/plugins/sp/lib/idea-handoff.generated.mjs +72 -11
- package/plugins/sp/lib/inline-run-app.ts +28 -0
- package/plugins/sp/lib/inline-run.generated.d.mts +6 -0
- package/plugins/sp/lib/inline-run.generated.mjs +364 -46
- package/plugins/sp/lib/plan-projection.generated.d.mts +28 -0
- package/plugins/sp/lib/plan-projection.generated.mjs +255 -0
- package/plugins/sp/lib/quality-gate.generated.d.mts +47 -1
- package/plugins/sp/lib/quality-gate.generated.mjs +30489 -82
- package/plugins/sp/lib/residual-scan.generated.d.mts +19 -0
- package/plugins/sp/lib/residual-scan.generated.mjs +66 -0
- package/plugins/sp/lib/run-summary-core.ts +561 -0
- package/plugins/sp/lib/transcript.ts +275 -0
- package/plugins/sp/plugin.json +1 -1
- package/plugins/sp/references/roles.md +1 -1
- package/plugins/sp/scripts/batch-plan.mjs +335 -0
- package/plugins/sp/scripts/batch-plan.ts +100 -0
- package/plugins/sp/scripts/batch-preflight.mjs +51 -3
- package/plugins/sp/scripts/batch-preflight.ts +72 -1
- package/plugins/sp/scripts/commit-guard.mjs +216 -0
- package/plugins/sp/scripts/commit-guard.ts +249 -0
- package/plugins/sp/scripts/history-anatomy-cache.mjs +5 -0
- package/plugins/sp/scripts/inline-run-dispatch.mjs +130 -0
- package/plugins/sp/scripts/inline-run-dispatch.ts +135 -0
- package/plugins/sp/scripts/inline-run-setup.mjs +563 -91
- package/plugins/sp/scripts/inline-run-setup.ts +65 -97
- package/plugins/sp/scripts/persist-out-check.mjs +90 -41
- package/plugins/sp/scripts/persist-out-check.ts +112 -57
- package/plugins/sp/scripts/quality-gate.mjs +30407 -73
- package/plugins/sp/scripts/quality-gate.ts +21 -5
- package/plugins/sp/scripts/residual-scan.mjs +88 -38
- package/plugins/sp/scripts/residual-scan.ts +35 -38
- package/plugins/sp/scripts/run-summary.mjs +693 -0
- package/plugins/sp/scripts/run-summary.ts +105 -0
- package/plugins/sp/scripts/session-timeline.mjs +331 -0
- package/plugins/sp/scripts/session-timeline.ts +204 -0
- package/plugins/sp/scripts/wrapup-steps.mjs +150 -2
- package/plugins/sp/scripts/wrapup-steps.ts +203 -1
- package/plugins/sp/skills/brainstorm/SKILL.md +9 -47
- package/plugins/sp/skills/branch-workflow/SKILL.md +1 -0
- package/plugins/sp/skills/branch-workflow/references/worktree-patterns.md +1 -0
- package/plugins/sp/skills/code-implementation/SKILL.md +48 -1
- package/plugins/sp/skills/code-verification/SKILL.md +8 -4
- package/plugins/sp/skills/code-verification/references/secu-review.md +3 -0
- package/plugins/sp/skills/code-verification/references/verdict-schema.md +68 -0
- package/plugins/sp/skills/fleet-join/SKILL.md +63 -0
- package/plugins/sp/skills/session-review/SKILL.md +77 -24
- package/plugins/sp/skills/source-driven-development/SKILL.md +5 -0
- package/plugins/sp/skills/spec-decomposition/references/decomposition.md +5 -3
- package/plugins/sp/skills/spur-cli/SKILL.md +6 -2
- package/plugins/sp/skills/spur-cli/references/agent.md +124 -17
- package/plugins/sp/skills/spur-cli/references/decision.md +98 -0
- package/plugins/sp/skills/spur-cli/references/message.md +7 -7
- package/plugins/sp/skills/spur-cli/references/tasks/verbs.md +41 -3
- package/plugins/sp/skills/spur-cli/references/tasks.md +10 -7
- package/plugins/sp/skills/spur-cli/references/workflows/authoring-workflows.md +7 -0
- package/plugins/sp/skills/spur-cli/references/workflows.md +7 -2
- package/plugins/sp/skills/spur-composer/SKILL.md +28 -2
- package/plugins/sp/skills/spur-dev/SKILL.md +18 -14
- package/plugins/sp/skills/spur-dev/references/ac-style-guide.md +6 -3
- package/plugins/sp/skills/spur-dev/references/cross-cutting.md +49 -14
- package/plugins/sp/skills/spur-dev/references/dev-operations.md +51 -18
- package/plugins/sp/skills/spur-dev/references/execution-batch-continuation.md +68 -0
- package/plugins/sp/skills/spur-dev/references/execution-batch-report.md +194 -0
- package/plugins/sp/skills/spur-dev/references/execution-batch.md +113 -963
- package/plugins/sp/skills/spur-dev/references/execution-parallel-isolation.md +130 -0
- package/plugins/sp/skills/spur-dev/references/execution-workflow.md +20 -0
- package/plugins/sp/skills/spur-dev/references/execution-worktree-landing.md +471 -0
- package/plugins/sp/skills/spur-dev/references/execution-worktree-setup.md +379 -0
- package/plugins/sp/skills/spur-dev/references/flag-glossary.md +51 -9
- package/plugins/sp/skills/spur-dev/references/glossary.md +2 -2
- package/plugins/sp/skills/spur-dev/references/inline-pipeline-driver.md +361 -156
- package/plugins/sp/skills/spur-dev/references/planning-workflow.md +6 -0
- package/plugins/sp/skills/spur-dev/references/structured-trace-emission.md +178 -0
- package/plugins/sp/skills/wayfinder/SKILL.md +10 -14
- package/schemas/decision-catalog.schema.json +226 -0
- package/schemas/spur-config.schema.json +25 -0
- package/schemas/state-machine-workflow.schema.json +23 -0
- package/spur.js +28165 -19051
- package/web/_astro/{BoardApp.i4eh_fHA.js → BoardApp.D66k_s_T.js} +1 -1
- package/web/_astro/BoardApp.D6sjAJr_.js +324 -0
- package/web/_astro/{TaskDetail.C5Cu8LdB.js → TaskDetail.BynLnMNc.js} +1 -1
- package/web/_astro/{arc.Bu5XpBEz.js → arc._BUadrdH.js} +1 -1
- package/web/_astro/{architectureDiagram-3BPJPVTR.9vHQTvhF.js → architectureDiagram-3BPJPVTR.Dh7xS1Pp.js} +1 -1
- package/web/_astro/{blockDiagram-GPEHLZMM.Cn2Xl7D6.js → blockDiagram-GPEHLZMM.DFDVpfXm.js} +1 -1
- package/web/_astro/{c4Diagram-AAUBKEIU.7o0P_QDo.js → c4Diagram-AAUBKEIU.V7_3_glh.js} +1 -1
- package/web/_astro/channel.DZVfto-r.js +1 -0
- package/web/_astro/{chunk-2J33WTMH.C3zZFsNt.js → chunk-2J33WTMH.De1yx7Jz.js} +1 -1
- package/web/_astro/{chunk-4BX2VUAB.CNy_M7fO.js → chunk-4BX2VUAB.BDhHFuxx.js} +1 -1
- package/web/_astro/{chunk-55IACEB6.2TiPnLBL.js → chunk-55IACEB6.DDLkWnmC.js} +1 -1
- package/web/_astro/{chunk-727SXJPM.5CGSyKAp.js → chunk-727SXJPM.CwfiLnA7.js} +1 -1
- package/web/_astro/{chunk-AQP2D5EJ.w-ZIgfsK.js → chunk-AQP2D5EJ.CtGDmUnM.js} +1 -1
- package/web/_astro/{chunk-FMBD7UC4.BkcEyhKa.js → chunk-FMBD7UC4.BOLjpw73.js} +1 -1
- package/web/_astro/{chunk-ND2GUHAM.mxKKbBG4.js → chunk-ND2GUHAM.BDhQls6O.js} +1 -1
- package/web/_astro/{chunk-QZHKN3VN.cyA0z9aT.js → chunk-QZHKN3VN.76qBynDF.js} +1 -1
- package/web/_astro/{classDiagram-4FO5ZUOK.CqtUzwhX.js → classDiagram-4FO5ZUOK.N0mpPHT0.js} +1 -1
- package/web/_astro/{classDiagram-v2-Q7XG4LA2.CqtUzwhX.js → classDiagram-v2-Q7XG4LA2.N0mpPHT0.js} +1 -1
- package/web/_astro/{cose-bilkent-S5V4N54A.BExPTopI.js → cose-bilkent-S5V4N54A.CKknFfjB.js} +1 -1
- package/web/_astro/{cynefin-OW5HDTMX.Bq2i10Cn.js → cynefin-OW5HDTMX.Cw3ZWBAh.js} +1 -1
- package/web/_astro/{dagre-BM42HDAG.By_HsOuv.js → dagre-BM42HDAG.D3yIKCRD.js} +1 -1
- package/web/_astro/{diagram-2AECGRRQ.BtU0b7hz.js → diagram-2AECGRRQ.D51dO83Z.js} +1 -1
- package/web/_astro/{diagram-5GNKFQAL.D00ztMdT.js → diagram-5GNKFQAL.Bb-FViDj.js} +1 -1
- package/web/_astro/{diagram-KO2AKTUF.DMVJWaUN.js → diagram-KO2AKTUF.D3mN8S6x.js} +1 -1
- package/web/_astro/{diagram-LMA3HP47.Cxqrjg5-.js → diagram-LMA3HP47.dMMyPcbJ.js} +1 -1
- package/web/_astro/{diagram-OG6HWLK6.2lz_1B5h.js → diagram-OG6HWLK6.AzbvzY4C.js} +1 -1
- package/web/_astro/{erDiagram-TEJ5UH35.BVQEuJ3T.js → erDiagram-TEJ5UH35.BR5I4OZb.js} +1 -1
- package/web/_astro/{flowDiagram-I6XJVG4X.Qlyo90ky.js → flowDiagram-I6XJVG4X.DFqvhGkn.js} +1 -1
- package/web/_astro/{ganttDiagram-6RSMTGT7.DRwb8HHZ.js → ganttDiagram-6RSMTGT7.ByJbhH6J.js} +1 -1
- package/web/_astro/{gitGraphDiagram-PVQCEYII.YfoPthfS.js → gitGraphDiagram-PVQCEYII.CInHjQR3.js} +1 -1
- package/web/_astro/index.COh_kBxN.css +1 -0
- package/web/_astro/{infoDiagram-5YYISTIA.DvFREck1.js → infoDiagram-5YYISTIA.Cm0dGVjJ.js} +1 -1
- package/web/_astro/{ishikawaDiagram-YF4QCWOH.BzCj9Q0-.js → ishikawaDiagram-YF4QCWOH.C3nOP6Sj.js} +1 -1
- package/web/_astro/{journeyDiagram-JHISSGLW.BDMT_AFb.js → journeyDiagram-JHISSGLW.4IvLa2MW.js} +1 -1
- package/web/_astro/{kanban-definition-UN3LZRKU.D6Fh3gj9.js → kanban-definition-UN3LZRKU.DS87Cl-k.js} +1 -1
- package/web/_astro/{linear.Dxw00fGQ.js → linear.MzmRrNER.js} +1 -1
- package/web/_astro/{mermaid.core.CsoxUWa0.js → mermaid.core.fK-WNswS.js} +4 -4
- package/web/_astro/{mindmap-definition-RKZ34NQL.CN8oeHLu.js → mindmap-definition-RKZ34NQL.BqipOHhY.js} +1 -1
- package/web/_astro/{pieDiagram-4H26LBE5.C7ADqQ_7.js → pieDiagram-4H26LBE5.0nBiPqFG.js} +1 -1
- package/web/_astro/{quadrantDiagram-W4KKPZXB.Cw9lKyHP.js → quadrantDiagram-W4KKPZXB.Szj7Zpx5.js} +1 -1
- package/web/_astro/{requirementDiagram-4Y6WPE33.B1ni-z4q.js → requirementDiagram-4Y6WPE33.D4UR6TWo.js} +1 -1
- package/web/_astro/{sankeyDiagram-5OEKKPKP.BhexdjVr.js → sankeyDiagram-5OEKKPKP.DAE7BdOs.js} +1 -1
- package/web/_astro/{sequenceDiagram-3UESZ5HK.C3Rm7dpQ.js → sequenceDiagram-3UESZ5HK.BP9ZVq25.js} +1 -1
- package/web/_astro/{stateDiagram-AJRCARHV.CkuMt27u.js → stateDiagram-AJRCARHV.BQW7mIxk.js} +1 -1
- package/web/_astro/{stateDiagram-v2-BHNVJYJU.Dc_aazUA.js → stateDiagram-v2-BHNVJYJU.PbK4nDih.js} +1 -1
- package/web/_astro/{timeline-definition-PNZ67QCA.ja7sUFhH.js → timeline-definition-PNZ67QCA.Ci_GvUpo.js} +1 -1
- package/web/_astro/{vennDiagram-CIIHVFJN.BxIshX_K.js → vennDiagram-CIIHVFJN.DNw1Y5qx.js} +1 -1
- package/web/_astro/{wardleyDiagram-YWT4CUSO.DfHlC0sh.js → wardleyDiagram-YWT4CUSO.CwlkaaNe.js} +1 -1
- package/web/_astro/{xychartDiagram-2RQKCTM6.DSnhHwCh.js → xychartDiagram-2RQKCTM6.MI6ZuiH_.js} +1 -1
- package/web/board-runtime.json +1 -1
- package/web/index.html +2 -2
- package/web/_astro/BoardApp.BVp-LYi6.js +0 -322
- package/web/_astro/channel.6IFIKBhu.js +0 -1
- package/web/_astro/index.CbNz17Sx.css +0 -1
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/**
|
|
3
|
+
* commit-guard — refuse a driver commit that would sweep a *foreign* writer's working-tree changes
|
|
4
|
+
* (task 1129, feature H1). Incidents: `a94f9f431` (a batch pipeline commit swept another session's
|
|
5
|
+
* uncommitted files) and `36f274590` (a close-out commit swept two other tasks' files through
|
|
6
|
+
* `git add -A`). The runbooks already said "stage only what the batch wrote"; nothing enforced it.
|
|
7
|
+
*
|
|
8
|
+
* Read/write `.spur/run/<id>-tree-start.json`; stage only the listed paths this run wrote; report
|
|
9
|
+
* the ones it did not. "Written by this run" = "changed since base" (task-diffstat semantics) minus
|
|
10
|
+
* start-dirty paths whose content still matches the start snapshot — a path dirty at start and
|
|
11
|
+
* untouched by the run belongs to another writer. Exit codes: 0 = staged all requested · 2 = some
|
|
12
|
+
* refused as foreign · 3 = conflict or unmerged target (nothing staged) · 1 = mis-invocation, or a
|
|
13
|
+
* missing start snapshot (`start` first).
|
|
14
|
+
*
|
|
15
|
+
* `node:*` builtins and relative imports only (sp-plugin-standalone); `options.git` is the test
|
|
16
|
+
* seam; `.spur/` is excluded from both sets (run bookkeeping is not product work — task-diffstat).
|
|
17
|
+
*
|
|
18
|
+
* shortcut: a foreign edit to a path that was CLEAN at `start` is indistinguishable from this run's
|
|
19
|
+
* own write — git records content, not authorship — so it is caught only once it is dirty in the
|
|
20
|
+
* start snapshot. Upgrade path: a per-run write journal if that case ever recurs.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import { spawnSync } from 'node:child_process';
|
|
24
|
+
import { mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs';
|
|
25
|
+
import { join } from 'node:path';
|
|
26
|
+
|
|
27
|
+
/** Base directory for the probes; `git` is the test seam (a fake runner instead of spawning). */
|
|
28
|
+
export interface GuardOptions {
|
|
29
|
+
cwd?: string;
|
|
30
|
+
base?: string;
|
|
31
|
+
git?: GitRunner;
|
|
32
|
+
}
|
|
33
|
+
/** Minimal spawnable git surface — tests fake this to return canned probe output. */
|
|
34
|
+
export interface GitRunResult {
|
|
35
|
+
status: number;
|
|
36
|
+
stdout: string;
|
|
37
|
+
}
|
|
38
|
+
export type GitRunner = (args: string[]) => GitRunResult;
|
|
39
|
+
export interface TreeStart {
|
|
40
|
+
head: string;
|
|
41
|
+
/** Working-file blob hashes at start; `sha` is null when the path did not exist. */
|
|
42
|
+
dirty: Array<{ path: string; sha: string | null }>;
|
|
43
|
+
}
|
|
44
|
+
/** `exitCode` is what `main` returns: 0 all staged · 2 some refused · 3 conflict · 1 usage. */
|
|
45
|
+
export type StageResult = { staged: string[]; foreign: string[]; exitCode: number };
|
|
46
|
+
|
|
47
|
+
export const COMMIT_GUARD_USAGE =
|
|
48
|
+
'usage: commit-guard start --run <id> | stage --run <id> [--base <sha>] -- <paths…> | check --run <id>';
|
|
49
|
+
|
|
50
|
+
/** Run bookkeeping is not product work — neither set may contain it (task-diffstat's rule). */
|
|
51
|
+
function isBookkeeping(path: string): boolean {
|
|
52
|
+
return path === '.spur' || path.startsWith('.spur/');
|
|
53
|
+
}
|
|
54
|
+
/** Probe/artifact paths are repo-relative; `cwd` only anchors them for tests and callers. */
|
|
55
|
+
function at(options: GuardOptions, rel: string): string {
|
|
56
|
+
return options.cwd ? join(options.cwd, rel) : rel;
|
|
57
|
+
}
|
|
58
|
+
function artifactPath(run: string): string {
|
|
59
|
+
return join('.spur', 'run', `${run}-tree-start.json`);
|
|
60
|
+
}
|
|
61
|
+
function gitOf(options: GuardOptions): GitRunner {
|
|
62
|
+
if (options.git) return options.git;
|
|
63
|
+
return (args: string[]): GitRunResult => {
|
|
64
|
+
const run = spawnSync('git', args, { cwd: options.cwd, encoding: 'utf8' });
|
|
65
|
+
return { status: run.status ?? 1, stdout: run.stdout ?? '' };
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
/** Working-file blob hash, or null when the path is absent (`git hash-object` failed). */
|
|
69
|
+
function hashPath(path: string, git: GitRunner): string | null {
|
|
70
|
+
const run = git(['hash-object', '--', path]);
|
|
71
|
+
return run.status === 0 ? run.stdout.trim() : null;
|
|
72
|
+
}
|
|
73
|
+
/** Paths from `git status --porcelain=v1 -z` (`XY <path>` entries; a rename's source token is
|
|
74
|
+
* consumed, not registered). */
|
|
75
|
+
export function parsePorcelainZ(stdout: string): string[] {
|
|
76
|
+
const tokens = stdout.split('\u0000');
|
|
77
|
+
const paths: string[] = [];
|
|
78
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
79
|
+
const entry = tokens[i] ?? '';
|
|
80
|
+
if (entry.length < 4) continue;
|
|
81
|
+
const path = entry.slice(3);
|
|
82
|
+
if (!isBookkeeping(path)) paths.push(path);
|
|
83
|
+
if (/[RC]/.test(entry.slice(0, 2))) i++;
|
|
84
|
+
}
|
|
85
|
+
return paths;
|
|
86
|
+
}
|
|
87
|
+
/** An unresolved conflict marker: an opener or closer line, or a `=======` between them — a `===`
|
|
88
|
+
* setext underline in prose is not a marker without an opener. */
|
|
89
|
+
export function conflictMarkerLine(content: string): boolean {
|
|
90
|
+
let opener = false;
|
|
91
|
+
for (const line of content.split('\n')) {
|
|
92
|
+
if (/^<{7}( |$)/.test(line)) opener = true;
|
|
93
|
+
else if (/^={7}(\s|$)/.test(line) && opener) return true;
|
|
94
|
+
else if (/^>{7}( |$)/.test(line)) return true;
|
|
95
|
+
}
|
|
96
|
+
return opener;
|
|
97
|
+
}
|
|
98
|
+
/** "Changed since base" (task-diffstat semantics): tracked changes + untracked, minus bookkeeping. */
|
|
99
|
+
function changedSinceBase(base: string, git: GitRunner): Set<string> {
|
|
100
|
+
const tracked = git(['diff', '--name-only', base]);
|
|
101
|
+
const others = git(['ls-files', '--others', '--exclude-standard', '-z']);
|
|
102
|
+
if (tracked.status !== 0 || others.status !== 0) return new Set();
|
|
103
|
+
const clean = (out: string, sep: string): string[] =>
|
|
104
|
+
out
|
|
105
|
+
.split(sep)
|
|
106
|
+
.map((p) => p.trim())
|
|
107
|
+
.filter((p) => p.length > 0 && !isBookkeeping(p));
|
|
108
|
+
return new Set([...clean(tracked.stdout, '\n'), ...clean(others.stdout, '\u0000')]);
|
|
109
|
+
}
|
|
110
|
+
function readTreeStart(run: string, options: GuardOptions): TreeStart | null {
|
|
111
|
+
try {
|
|
112
|
+
const parsed = JSON.parse(readFileSync(at(options, artifactPath(run)), 'utf8')) as TreeStart;
|
|
113
|
+
if (typeof parsed.head === 'string' && Array.isArray(parsed.dirty)) return parsed;
|
|
114
|
+
} catch {
|
|
115
|
+
/* absent or malformed — the caller fails closed */
|
|
116
|
+
}
|
|
117
|
+
return null;
|
|
118
|
+
}
|
|
119
|
+
/** Start snapshot + resolved base + the paths this run wrote: "changed since base", keeping only
|
|
120
|
+
* start-dirty paths whose content its own edits changed. */
|
|
121
|
+
function runContext(run: string, options: GuardOptions) {
|
|
122
|
+
const git = gitOf(options);
|
|
123
|
+
const start = readTreeStart(run, options);
|
|
124
|
+
if (!start) return null;
|
|
125
|
+
let base = options.base ?? '';
|
|
126
|
+
if (!base) {
|
|
127
|
+
try {
|
|
128
|
+
const anchored = readFileSync(at(options, join('.spur', 'run', `${run}-base.sha`)), 'utf8').trim();
|
|
129
|
+
if (/^[0-9a-f]{7,40}$/i.test(anchored)) base = anchored;
|
|
130
|
+
} catch {
|
|
131
|
+
/* no precheck anchor — the start head is the base */
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
if (!base) base = start.head.length > 0 ? start.head : 'HEAD';
|
|
135
|
+
const written = changedSinceBase(base, git);
|
|
136
|
+
for (const entry of start.dirty) if (hashPath(entry.path, git) === entry.sha) written.delete(entry.path);
|
|
137
|
+
return { git, base, start, written };
|
|
138
|
+
}
|
|
139
|
+
function missingStart(run: string): number {
|
|
140
|
+
process.stderr.write(`commit-guard: no ${artifactPath(run)} — run \`commit-guard start --run ${run}\` first\n`);
|
|
141
|
+
return 1;
|
|
142
|
+
}
|
|
143
|
+
/** Fingerprint the tree once, before the run writes anything (R1). */
|
|
144
|
+
export function runStart(run: string, options: GuardOptions = {}): number {
|
|
145
|
+
const git = gitOf(options);
|
|
146
|
+
const head = git(['rev-parse', 'HEAD']);
|
|
147
|
+
const status = git(['status', '--porcelain=v1', '-z']);
|
|
148
|
+
if (head.status !== 0 || status.status !== 0) {
|
|
149
|
+
process.stderr.write('commit-guard: git probe failed (rev-parse HEAD / status) — is this a work tree?\n');
|
|
150
|
+
return 1;
|
|
151
|
+
}
|
|
152
|
+
const snapshot: TreeStart = {
|
|
153
|
+
head: head.stdout.trim(),
|
|
154
|
+
dirty: parsePorcelainZ(status.stdout).map((path) => ({ path, sha: hashPath(path, git) })),
|
|
155
|
+
};
|
|
156
|
+
const abs = at(options, artifactPath(run));
|
|
157
|
+
mkdirSync(join(abs, '..'), { recursive: true });
|
|
158
|
+
writeFileSync(`${abs}.tmp`, `${JSON.stringify(snapshot, null, 2)}\n`);
|
|
159
|
+
renameSync(`${abs}.tmp`, abs); // atomic: a concurrent reader never sees a half-written snapshot
|
|
160
|
+
return 0;
|
|
161
|
+
}
|
|
162
|
+
/** The first path that is unmerged or carries conflict markers, or null (R4, fail closed). */
|
|
163
|
+
function conflictingTarget(paths: string[], options: GuardOptions, git: GitRunner): string | null {
|
|
164
|
+
const unmerged = new Set(
|
|
165
|
+
git(['diff', '--name-only', '--diff-filter=U'])
|
|
166
|
+
.stdout.split('\n')
|
|
167
|
+
.map((p) => p.trim()),
|
|
168
|
+
);
|
|
169
|
+
for (const path of paths) {
|
|
170
|
+
if (unmerged.has(path)) return `${path} (unmerged)`;
|
|
171
|
+
try {
|
|
172
|
+
if (conflictMarkerLine(readFileSync(at(options, path), 'utf8'))) return `${path} (conflict markers)`;
|
|
173
|
+
} catch {
|
|
174
|
+
/* deleted target — nothing to scan */
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
return null;
|
|
178
|
+
}
|
|
179
|
+
/** Stage only the listed paths this run wrote (R2) and print `{staged, foreign}` (AC1). */
|
|
180
|
+
export function runStage(run: string, paths: string[], options: GuardOptions = {}): StageResult {
|
|
181
|
+
if (paths.length === 0) {
|
|
182
|
+
process.stderr.write(`${COMMIT_GUARD_USAGE}\n`);
|
|
183
|
+
return { staged: [], foreign: [], exitCode: 1 };
|
|
184
|
+
}
|
|
185
|
+
const context = runContext(run, options);
|
|
186
|
+
if (!context) return { staged: [], foreign: [], exitCode: missingStart(run) };
|
|
187
|
+
const conflict = conflictingTarget(paths, options, context.git);
|
|
188
|
+
if (conflict) {
|
|
189
|
+
process.stderr.write(`commit-guard: refusing to stage ${conflict}; nothing staged\n`);
|
|
190
|
+
return { staged: [], foreign: [], exitCode: 3 };
|
|
191
|
+
}
|
|
192
|
+
const dirtyAtStart = new Set(context.start.dirty.map((entry) => entry.path));
|
|
193
|
+
const staged = paths.filter((path) => context.written.has(path));
|
|
194
|
+
const refused = paths.filter((path) => !context.written.has(path));
|
|
195
|
+
for (const path of refused) {
|
|
196
|
+
const why = dirtyAtStart.has(path) ? 'foreign — dirty at start, untouched by this run' : 'unchanged since base';
|
|
197
|
+
process.stderr.write(`commit-guard: refused ${path} (${why})\n`);
|
|
198
|
+
}
|
|
199
|
+
if (staged.length > 0 && context.git(['add', '--', ...staged]).status !== 0) {
|
|
200
|
+
process.stderr.write('commit-guard: git add failed — nothing staged\n');
|
|
201
|
+
return { staged: [], foreign: [], exitCode: 1 };
|
|
202
|
+
}
|
|
203
|
+
const foreign = refused.filter((path) => dirtyAtStart.has(path));
|
|
204
|
+
process.stdout.write(`${JSON.stringify({ staged, foreign })}\n`);
|
|
205
|
+
return { staged, foreign, exitCode: refused.length > 0 ? 2 : 0 };
|
|
206
|
+
}
|
|
207
|
+
/** Report the working-tree changes this run did not write, staging nothing (R3). */
|
|
208
|
+
export function runCheck(run: string, options: GuardOptions = {}): number {
|
|
209
|
+
const context = runContext(run, options);
|
|
210
|
+
if (!context) return missingStart(run);
|
|
211
|
+
const foreign = [...changedSinceBase(context.base, context.git)]
|
|
212
|
+
.filter((path) => !context.written.has(path))
|
|
213
|
+
.sort();
|
|
214
|
+
process.stdout.write(`${JSON.stringify({ foreign })}\n`);
|
|
215
|
+
return 0;
|
|
216
|
+
}
|
|
217
|
+
/** Pull one `--flag value` pair out of argv, or undefined when absent (a missing value is usage). */
|
|
218
|
+
function takeFlag(args: string[], name: string): string | undefined {
|
|
219
|
+
const index = args.indexOf(name);
|
|
220
|
+
const value = index === -1 ? undefined : args[index + 1];
|
|
221
|
+
const missing = value === undefined || value.startsWith('--');
|
|
222
|
+
if (index !== -1) args.splice(index, missing ? 1 : 2);
|
|
223
|
+
return missing ? undefined : value;
|
|
224
|
+
}
|
|
225
|
+
export function main(argv: string[], options: GuardOptions = {}): number {
|
|
226
|
+
const args = [...argv];
|
|
227
|
+
takeFlag(args, '--spur-bin'); // 0482 R2: pipelines hand every plugin script this flag
|
|
228
|
+
const sub = args.shift();
|
|
229
|
+
const run = takeFlag(args, '--run');
|
|
230
|
+
const base = takeFlag(args, '--base');
|
|
231
|
+
const usage = (): number => {
|
|
232
|
+
process.stderr.write(`${COMMIT_GUARD_USAGE}\n`);
|
|
233
|
+
return 1;
|
|
234
|
+
};
|
|
235
|
+
if (!run) return usage();
|
|
236
|
+
if (sub === 'start') return args.length === 0 ? runStart(run, options) : usage();
|
|
237
|
+
if (sub === 'check') return args.length === 0 ? runCheck(run, { ...options, base }) : usage();
|
|
238
|
+
if (sub === 'stage')
|
|
239
|
+
return runStage(
|
|
240
|
+
run,
|
|
241
|
+
args.filter((arg) => arg !== '--'),
|
|
242
|
+
{ ...options, base },
|
|
243
|
+
).exitCode;
|
|
244
|
+
return usage();
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
if (import.meta.main) {
|
|
248
|
+
process.exit(main(process.argv.slice(2)));
|
|
249
|
+
}
|
|
@@ -264,6 +264,7 @@ var FINDING_CATEGORIES = [
|
|
|
264
264
|
"telemetry",
|
|
265
265
|
"positive"
|
|
266
266
|
];
|
|
267
|
+
var FINDING_CONFIDENCES = ["high", "medium", "low"];
|
|
267
268
|
function checkReportStructure(reportMarkdown) {
|
|
268
269
|
const problems = [];
|
|
269
270
|
if (/TODO|PLACEHOLDER|FIXME|^\|\s*\|/im.test(reportMarkdown))
|
|
@@ -308,6 +309,10 @@ function checkReportStructure(reportMarkdown) {
|
|
|
308
309
|
if (!/(^|[\s`])P[123]([\s`.]|$)/.test(block) && !block.includes("symbolic-severity")) {
|
|
309
310
|
problems.push("finding-invalid-severity");
|
|
310
311
|
}
|
|
312
|
+
const confValue = block.match(/`confidence`\s*[:=]\s*`?([^`\n]*?)`?\s*(?:\n|$)/)?.[1]?.trim();
|
|
313
|
+
if (confValue !== undefined && confValue !== "" && !FINDING_CONFIDENCES.includes(confValue.toLowerCase())) {
|
|
314
|
+
problems.push(`finding-invalid-confidence:${confValue}`);
|
|
315
|
+
}
|
|
311
316
|
const catValue = block.match(/`category`\s*[:=]\s*`?([^`\n]+?)`?\s*(?:\n|$)/)?.[1];
|
|
312
317
|
if (catValue && !FINDING_CATEGORIES.includes(catValue.trim())) {
|
|
313
318
|
problems.push(`finding-invalid-category:${catValue.trim()}`);
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// @bun
|
|
3
|
+
|
|
4
|
+
// plugins/sp/scripts/inline-run-dispatch.ts
|
|
5
|
+
import { spawnSync } from "child_process";
|
|
6
|
+
import { existsSync } from "fs";
|
|
7
|
+
import { dirname, join, resolve } from "path";
|
|
8
|
+
import { fileURLToPath } from "url";
|
|
9
|
+
|
|
10
|
+
// plugins/sp/lib/env.ts
|
|
11
|
+
function getEnvVar(name, fallback) {
|
|
12
|
+
const raw = process.env[name];
|
|
13
|
+
return raw === undefined ? fallback : raw;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// plugins/sp/lib/inline-run-app.ts
|
|
17
|
+
async function loadInlineApp(spurBin, resolveAppEntry, required) {
|
|
18
|
+
const { entry, portable } = resolveAppEntry(spurBin);
|
|
19
|
+
const app = await import(entry);
|
|
20
|
+
const missing = portable ? required.filter((name) => typeof app[name] !== "function") : [];
|
|
21
|
+
if (missing.length > 0) {
|
|
22
|
+
throw new Error(`inline application bundle ${entry} is older than its scripts — missing ${missing.join(", ")}. Reinstall sp with a superskill release that stages lib/ (commit b42961b), or pass --spur-bin <spur checkout>/apps/cli/src/index.ts.`);
|
|
23
|
+
}
|
|
24
|
+
return { app, portable };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// plugins/sp/scripts/inline-run-dispatch.ts
|
|
28
|
+
function usage() {
|
|
29
|
+
console.error([
|
|
30
|
+
"Usage: bun plugins/sp/scripts/inline-run-dispatch.ts --attribution --run-id <id> --stage <state> [--executor <name>] [--agent <name>] [--model <m>] [--spur-bin <path>]",
|
|
31
|
+
" bun plugins/sp/scripts/inline-run-dispatch.ts --dispatch-failure --run-id <id> --stage <state> --decision <escalate|stop|host-inline> (--text <record> | --text-file <path>) [--executor <name>] [--agent <name>] [--model <m>] [--spur-bin <path>]",
|
|
32
|
+
" --attribution writes .spur/run/<run-id>-attribution.jsonl; an omitted/empty --executor records the explicit no-attribution marker.",
|
|
33
|
+
" --dispatch-failure classifies the record upstream, writes .spur/run/<run-id>-dispatch-fallback.json, and carries an attributed capacity exhaustion onto the durable availability path."
|
|
34
|
+
].join(`
|
|
35
|
+
`));
|
|
36
|
+
process.exit(2);
|
|
37
|
+
}
|
|
38
|
+
var SAFE_RUN_ID_RE = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
|
|
39
|
+
function resolveAppEntry(spurBin) {
|
|
40
|
+
const fallback = fileURLToPath(new URL("../../../apps/cli/src/index.ts", import.meta.url));
|
|
41
|
+
const candidates = spurBin !== "" ? [spurBin] : [fallback];
|
|
42
|
+
for (const candidate of candidates) {
|
|
43
|
+
const tokens = candidate.split(/\s+/).filter(Boolean);
|
|
44
|
+
const mainModule = [...tokens].reverse().find((t) => t.endsWith(".ts"));
|
|
45
|
+
if (mainModule === undefined || !existsSync(mainModule))
|
|
46
|
+
continue;
|
|
47
|
+
const repoRoot = resolve(dirname(mainModule), "..", "..", "..");
|
|
48
|
+
const appEntry = join(repoRoot, "packages", "app", "src", "index.ts");
|
|
49
|
+
if (existsSync(appEntry))
|
|
50
|
+
return { entry: appEntry, portable: false };
|
|
51
|
+
}
|
|
52
|
+
const entry = fileURLToPath(new URL("../lib/inline-run.generated.mjs", import.meta.url));
|
|
53
|
+
if (!existsSync(entry)) {
|
|
54
|
+
throw new Error("inline application bundle is missing \u2014 rebuild/install the sp plugin before running inline");
|
|
55
|
+
}
|
|
56
|
+
return { entry, portable: true };
|
|
57
|
+
}
|
|
58
|
+
async function main() {
|
|
59
|
+
if (!process.versions.bun) {
|
|
60
|
+
const child = spawnSync("bun", [fileURLToPath(import.meta.url), ...process.argv.slice(2)], {
|
|
61
|
+
stdio: "inherit"
|
|
62
|
+
});
|
|
63
|
+
if (child.error)
|
|
64
|
+
console.error(`inline-run-dispatch requires Bun on PATH: ${child.error.message}`);
|
|
65
|
+
process.exit(child.status ?? 1);
|
|
66
|
+
}
|
|
67
|
+
const flags = new Map;
|
|
68
|
+
let attribution = false;
|
|
69
|
+
let dispatchFailure = false;
|
|
70
|
+
let spurBin = getEnvVar("SPUR_BIN") ?? "";
|
|
71
|
+
const argv = process.argv.slice(2);
|
|
72
|
+
for (let i = 0;i < argv.length; i++) {
|
|
73
|
+
const flag = argv[i];
|
|
74
|
+
if (flag === "--attribution")
|
|
75
|
+
attribution = true;
|
|
76
|
+
else if (flag === "--dispatch-failure")
|
|
77
|
+
dispatchFailure = true;
|
|
78
|
+
else if (flag === "--spur-bin")
|
|
79
|
+
spurBin = argv[++i] ?? spurBin;
|
|
80
|
+
else if (flag !== undefined)
|
|
81
|
+
flags.set(flag, argv[++i] ?? "");
|
|
82
|
+
}
|
|
83
|
+
const runId = flags.get("--run-id") ?? "";
|
|
84
|
+
const stage = flags.get("--stage") ?? "";
|
|
85
|
+
const decision = flags.get("--decision") ?? "";
|
|
86
|
+
const text = flags.get("--text") ?? "";
|
|
87
|
+
const textFile = flags.get("--text-file") ?? "";
|
|
88
|
+
const executor = flags.get("--executor") ?? "";
|
|
89
|
+
const agent = flags.get("--agent") ?? "";
|
|
90
|
+
const model = flags.get("--model") ?? "";
|
|
91
|
+
if (attribution === dispatchFailure)
|
|
92
|
+
usage();
|
|
93
|
+
if (runId.trim() === "" || stage.trim() === "" || !SAFE_RUN_ID_RE.test(runId))
|
|
94
|
+
usage();
|
|
95
|
+
const identity = {
|
|
96
|
+
executor: executor.trim() === "" ? null : executor,
|
|
97
|
+
agent: agent.trim() === "" ? null : agent,
|
|
98
|
+
...model.trim() === "" ? {} : { model }
|
|
99
|
+
};
|
|
100
|
+
const { app, portable } = await loadInlineApp(spurBin, resolveAppEntry, [
|
|
101
|
+
"recordInlineRunAttribution",
|
|
102
|
+
"runInlineRunDispatchFailure"
|
|
103
|
+
]);
|
|
104
|
+
if (attribution) {
|
|
105
|
+
app.recordInlineRunAttribution(process.cwd(), runId, {
|
|
106
|
+
stage,
|
|
107
|
+
...identity,
|
|
108
|
+
observedAt: new Date().toISOString()
|
|
109
|
+
});
|
|
110
|
+
process.stdout.write(`${JSON.stringify({ ok: true, runId, stage })}
|
|
111
|
+
`);
|
|
112
|
+
process.exit(0);
|
|
113
|
+
}
|
|
114
|
+
if (decision.trim() === "" || text === "" === (textFile === ""))
|
|
115
|
+
usage();
|
|
116
|
+
const { app: lib } = await loadInlineApp(spurBin, () => ({ entry: fileURLToPath(new URL("../lib/inline-run.generated.mjs", import.meta.url)), portable }), ["loadSpurConfig"]);
|
|
117
|
+
const spurConfig = await lib.loadSpurConfig(process.cwd(), portable ? { embeddedSchemas: lib.EMBEDDED_SPUR_SCHEMAS } : undefined);
|
|
118
|
+
process.exit(await app.runInlineRunDispatchFailure({
|
|
119
|
+
runId,
|
|
120
|
+
stage,
|
|
121
|
+
decision,
|
|
122
|
+
...text === "" ? { textFile } : { text },
|
|
123
|
+
...identity,
|
|
124
|
+
spurConfig
|
|
125
|
+
}));
|
|
126
|
+
}
|
|
127
|
+
main().catch((e) => {
|
|
128
|
+
console.error(`inline-run-dispatch: FAIL \u2014 ${e instanceof Error ? e.message : String(e)}`);
|
|
129
|
+
process.exit(1);
|
|
130
|
+
});
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/** Inline dispatch fail-fast facade (task 1134 R1–R4): argv/env only. The classification, the
|
|
3
|
+
* decision artifact and the durable availability write all live in the app runners
|
|
4
|
+
* (`recordInlineRunAttribution`, `runInlineRunDispatchFailure`), reached through the same
|
|
5
|
+
* `loadInlineApp` seam the setup facade uses. It is a SEPARATE script from `inline-run-setup.ts`
|
|
6
|
+
* because it is a separate operation and the ADR-130 glue budget is per script. */
|
|
7
|
+
import { spawnSync } from 'node:child_process';
|
|
8
|
+
import { existsSync } from 'node:fs';
|
|
9
|
+
import { dirname, join, resolve } from 'node:path';
|
|
10
|
+
import { fileURLToPath } from 'node:url';
|
|
11
|
+
// Type-only namespace import (erased before bundling; the smoke scanner exempts `import type`).
|
|
12
|
+
import type * as spurApp from '@gobing-ai/spur-app';
|
|
13
|
+
import { getEnvVar } from '../lib/env';
|
|
14
|
+
import { loadInlineApp } from '../lib/inline-run-app';
|
|
15
|
+
|
|
16
|
+
// EMBEDDED_SPUR_SCHEMAS is declared by the bundle twin, never exported by the barrel.
|
|
17
|
+
type InlineApp = typeof spurApp & { EMBEDDED_SPUR_SCHEMAS?: ReadonlyMap<string, string> };
|
|
18
|
+
|
|
19
|
+
function usage(): never {
|
|
20
|
+
console.error(
|
|
21
|
+
[
|
|
22
|
+
'Usage: bun plugins/sp/scripts/inline-run-dispatch.ts --attribution --run-id <id> --stage <state> [--executor <name>] [--agent <name>] [--model <m>] [--spur-bin <path>]',
|
|
23
|
+
' bun plugins/sp/scripts/inline-run-dispatch.ts --dispatch-failure --run-id <id> --stage <state> --decision <escalate|stop|host-inline> (--text <record> | --text-file <path>) [--executor <name>] [--agent <name>] [--model <m>] [--spur-bin <path>]',
|
|
24
|
+
' --attribution writes .spur/run/<run-id>-attribution.jsonl; an omitted/empty --executor records the explicit no-attribution marker.',
|
|
25
|
+
' --dispatch-failure classifies the record upstream, writes .spur/run/<run-id>-dispatch-fallback.json, and carries an attributed capacity exhaustion onto the durable availability path.',
|
|
26
|
+
].join('\n'),
|
|
27
|
+
);
|
|
28
|
+
process.exit(2);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Run ids become filenames under `.spur/run/` — single safe component only (0804 R8). */
|
|
32
|
+
const SAFE_RUN_ID_RE = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
|
|
33
|
+
|
|
34
|
+
/** Resolve the app entry: --spur-bin/SPUR_BIN main module if it proves a repo checkout, else the bundle. */
|
|
35
|
+
function resolveAppEntry(spurBin: string): { entry: string; portable: boolean } {
|
|
36
|
+
const fallback = fileURLToPath(new URL('../../../apps/cli/src/index.ts', import.meta.url));
|
|
37
|
+
const candidates = spurBin !== '' ? [spurBin] : [fallback];
|
|
38
|
+
for (const candidate of candidates) {
|
|
39
|
+
const tokens = candidate.split(/\s+/).filter(Boolean);
|
|
40
|
+
// Main module = last path-like token; only a .ts source entry proves a repo checkout.
|
|
41
|
+
const mainModule = [...tokens].reverse().find((t) => t.endsWith('.ts'));
|
|
42
|
+
if (mainModule === undefined || !existsSync(mainModule)) continue;
|
|
43
|
+
const repoRoot = resolve(dirname(mainModule), '..', '..', '..');
|
|
44
|
+
const appEntry = join(repoRoot, 'packages', 'app', 'src', 'index.ts');
|
|
45
|
+
if (existsSync(appEntry)) return { entry: appEntry, portable: false };
|
|
46
|
+
}
|
|
47
|
+
const entry = fileURLToPath(new URL('../lib/inline-run.generated.mjs', import.meta.url));
|
|
48
|
+
if (!existsSync(entry)) {
|
|
49
|
+
throw new Error('inline application bundle is missing — rebuild/install the sp plugin before running inline');
|
|
50
|
+
}
|
|
51
|
+
return { entry, portable: true };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async function main(): Promise<void> {
|
|
55
|
+
// The portable Node twin has no workspace imports; the app bundle still uses Spur's Bun runtime.
|
|
56
|
+
if (!process.versions.bun) {
|
|
57
|
+
const child = spawnSync('bun', [fileURLToPath(import.meta.url), ...process.argv.slice(2)], {
|
|
58
|
+
stdio: 'inherit',
|
|
59
|
+
});
|
|
60
|
+
if (child.error) console.error(`inline-run-dispatch requires Bun on PATH: ${child.error.message}`);
|
|
61
|
+
process.exit(child.status ?? 1);
|
|
62
|
+
}
|
|
63
|
+
const flags = new Map<string, string>();
|
|
64
|
+
let attribution = false;
|
|
65
|
+
let dispatchFailure = false;
|
|
66
|
+
let spurBin = getEnvVar('SPUR_BIN') ?? '';
|
|
67
|
+
const argv = process.argv.slice(2);
|
|
68
|
+
for (let i = 0; i < argv.length; i++) {
|
|
69
|
+
const flag = argv[i];
|
|
70
|
+
if (flag === '--attribution') attribution = true;
|
|
71
|
+
else if (flag === '--dispatch-failure') dispatchFailure = true;
|
|
72
|
+
else if (flag === '--spur-bin') spurBin = argv[++i] ?? spurBin;
|
|
73
|
+
else if (flag !== undefined) flags.set(flag, argv[++i] ?? '');
|
|
74
|
+
}
|
|
75
|
+
const runId = flags.get('--run-id') ?? '';
|
|
76
|
+
const stage = flags.get('--stage') ?? '';
|
|
77
|
+
const decision = flags.get('--decision') ?? '';
|
|
78
|
+
const text = flags.get('--text') ?? '';
|
|
79
|
+
const textFile = flags.get('--text-file') ?? '';
|
|
80
|
+
const executor = flags.get('--executor') ?? '';
|
|
81
|
+
const agent = flags.get('--agent') ?? '';
|
|
82
|
+
const model = flags.get('--model') ?? '';
|
|
83
|
+
|
|
84
|
+
if (attribution === dispatchFailure) usage();
|
|
85
|
+
if (runId.trim() === '' || stage.trim() === '' || !SAFE_RUN_ID_RE.test(runId)) usage();
|
|
86
|
+
// R3: the executor identity the inline stage actually used. An omitted/empty `--executor` is the
|
|
87
|
+
// explicit no-attribution marker — never a silent drop.
|
|
88
|
+
const identity = {
|
|
89
|
+
executor: executor.trim() === '' ? null : executor,
|
|
90
|
+
agent: agent.trim() === '' ? null : agent,
|
|
91
|
+
...(model.trim() === '' ? {} : { model }),
|
|
92
|
+
};
|
|
93
|
+
const { app, portable } = await loadInlineApp<InlineApp>(spurBin, resolveAppEntry, [
|
|
94
|
+
'recordInlineRunAttribution',
|
|
95
|
+
'runInlineRunDispatchFailure',
|
|
96
|
+
]);
|
|
97
|
+
|
|
98
|
+
if (attribution) {
|
|
99
|
+
app.recordInlineRunAttribution(process.cwd(), runId, {
|
|
100
|
+
stage,
|
|
101
|
+
...identity,
|
|
102
|
+
observedAt: new Date().toISOString(),
|
|
103
|
+
});
|
|
104
|
+
process.stdout.write(`${JSON.stringify({ ok: true, runId, stage })}\n`);
|
|
105
|
+
process.exit(0);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (decision.trim() === '' || (text === '') === (textFile === '')) usage();
|
|
109
|
+
// ADR-082: this facade is the composition root, so it loads the effective config once and
|
|
110
|
+
// threads the merged result into the app runner (never a per-slice load in packages/app).
|
|
111
|
+
const { app: lib } = await loadInlineApp<typeof import('../lib/inline-run.generated.mjs')>(
|
|
112
|
+
spurBin,
|
|
113
|
+
() => ({ entry: fileURLToPath(new URL('../lib/inline-run.generated.mjs', import.meta.url)), portable }),
|
|
114
|
+
['loadSpurConfig'],
|
|
115
|
+
);
|
|
116
|
+
const spurConfig = await lib.loadSpurConfig(
|
|
117
|
+
process.cwd(),
|
|
118
|
+
portable ? { embeddedSchemas: lib.EMBEDDED_SPUR_SCHEMAS } : undefined,
|
|
119
|
+
);
|
|
120
|
+
process.exit(
|
|
121
|
+
await app.runInlineRunDispatchFailure({
|
|
122
|
+
runId,
|
|
123
|
+
stage,
|
|
124
|
+
decision: decision as 'escalate' | 'stop' | 'host-inline',
|
|
125
|
+
...(text === '' ? { textFile } : { text }),
|
|
126
|
+
...identity,
|
|
127
|
+
spurConfig,
|
|
128
|
+
}),
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
main().catch((e: unknown) => {
|
|
133
|
+
console.error(`inline-run-dispatch: FAIL — ${e instanceof Error ? e.message : String(e)}`);
|
|
134
|
+
process.exit(1);
|
|
135
|
+
});
|