amsd-pipeline-shared 0.5.0 → 0.5.2

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 (50) hide show
  1. package/docs/gateway-baseline-2.0.80.md5 +47 -0
  2. package/docs/gateway-files.txt +47 -0
  3. package/orchestrations/config/repo-artifacts.json +23 -0
  4. package/orchestrations/config/resume-preserves.json +25 -0
  5. package/orchestrations/config/run-modes.json +47 -0
  6. package/orchestrations/scripts/lib/checkpoints.sh +175 -0
  7. package/orchestrations/scripts/lib/codeline-lanes.sh +1664 -0
  8. package/orchestrations/scripts/lib/codeline-packages.js +67 -0
  9. package/orchestrations/scripts/lib/codeline-write-perimeter.sh +318 -0
  10. package/orchestrations/scripts/lib/common.sh +66 -0
  11. package/orchestrations/scripts/lib/control-plane.sh +506 -0
  12. package/orchestrations/scripts/lib/ecosystem-registry.js +148 -0
  13. package/orchestrations/scripts/lib/engine-paths.sh +52 -0
  14. package/orchestrations/scripts/lib/flags.sh +38 -0
  15. package/orchestrations/scripts/lib/git-ops.sh +975 -0
  16. package/orchestrations/scripts/lib/halt-recovery.sh +56 -0
  17. package/orchestrations/scripts/lib/handlers/_read-input.js +88 -0
  18. package/orchestrations/scripts/lib/handlers/carry-plan.js +25 -0
  19. package/orchestrations/scripts/lib/handlers/cl-entries.js +10 -0
  20. package/orchestrations/scripts/lib/handlers/codeline-ecosystem.js +164 -0
  21. package/orchestrations/scripts/lib/handlers/codeline-manifests.js +148 -0
  22. package/orchestrations/scripts/lib/handlers/filtered-prd.js +85 -0
  23. package/orchestrations/scripts/lib/handlers/merge-lane-into-canonical.js +39 -0
  24. package/orchestrations/scripts/lib/handlers/package-engines-node.js +25 -0
  25. package/orchestrations/scripts/lib/handlers/prd-phases.js +46 -0
  26. package/orchestrations/scripts/lib/handlers/rebuild-cross-contract.js +7 -0
  27. package/orchestrations/scripts/lib/handlers/record-declared-pass.js +23 -0
  28. package/orchestrations/scripts/lib/handlers/release-line-prd.js +31 -0
  29. package/orchestrations/scripts/lib/handlers/release-lines.js +31 -0
  30. package/orchestrations/scripts/lib/handlers/repo-exclude-patterns.js +108 -0
  31. package/orchestrations/scripts/lib/handlers/resume-spec-output-present.js +16 -0
  32. package/orchestrations/scripts/lib/handlers/sibling-providers.js +43 -0
  33. package/orchestrations/scripts/lib/handlers/spanning-stories-incomplete.js +50 -0
  34. package/orchestrations/scripts/lib/handlers/spec-inputs-digest.js +48 -0
  35. package/orchestrations/scripts/lib/handlers/spec-predates-plan.js +40 -0
  36. package/orchestrations/scripts/lib/handlers/stories-lacking-spec.js +64 -0
  37. package/orchestrations/scripts/lib/handlers/write-missing-plans.js +92 -0
  38. package/orchestrations/scripts/lib/orchestration-resume.sh +74 -0
  39. package/orchestrations/scripts/lib/phase-exit.sh +90 -0
  40. package/orchestrations/scripts/lib/pipeline-commit.sh +19 -0
  41. package/orchestrations/scripts/lib/plan-hash.js +19 -0
  42. package/orchestrations/scripts/lib/plan-scope.js +73 -0
  43. package/orchestrations/scripts/lib/qa-gate-evidence.sh +235 -0
  44. package/orchestrations/scripts/lib/release-lines.sh +166 -0
  45. package/orchestrations/scripts/lib/resume-semantics.sh +31 -0
  46. package/orchestrations/scripts/lib/run-checkpoint.sh +842 -0
  47. package/orchestrations/scripts/lib/run-modes.sh +93 -0
  48. package/orchestrations/scripts/lib/story-outputs.sh +322 -0
  49. package/orchestrations/scripts/lib/story-retry-state.sh +355 -0
  50. package/package.json +1 -1
@@ -0,0 +1,46 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * THE PHASES A PRD DECLARES, ONE PER LINE.
4
+ *
5
+ * The caller loops over these to run each phase of a codeline. An empty list means "this PRD
6
+ * declares no phases", and the loop then does nothing — so this must never return empty for any
7
+ * reason other than the PRD genuinely having none.
8
+ *
9
+ * IT DID. Extracted from an inline shell snippet, the body kept the shell's own placeholder:
10
+ *
11
+ * require('./_read-input.js').readJsonOrRefuse('$1', 'the PRD', { expect: 'object' })
12
+ *
13
+ * `$1` was never substituted, so it tried to open a file literally named `$1`, threw, and the
14
+ * caller's `2>/dev/null` swallowed it. Every parallel lane received an empty phase list, ran its
15
+ * loop zero times, and reported `✓ completed` — in five seconds, having done nothing. Live
16
+ * 2026-08-17, mock3: two lanes, two pending stories, no writer invoked, no commit, pipeline
17
+ * reported success.
18
+ *
19
+ * The caller passed the path all along; the function it called dropped the argument.
20
+ *
21
+ * argv[2] the PRD to read; falls back to $PRD_FILE
22
+ * stdout one phase id per line, in declared order
23
+ * exit 1 the PRD cannot be read or parsed — NOT an empty list, which reads as "no phases"
24
+ */
25
+ 'use strict';
26
+
27
+ const fs = require('fs');
28
+
29
+ const file = process.argv[2] || process.env.PRD_FILE || '';
30
+ if (!file) {
31
+ process.stderr.write('[prd-phases] no PRD given: pass a path or set PRD_FILE\n');
32
+ process.exit(1);
33
+ }
34
+
35
+ let prd;
36
+ try {
37
+ prd = require('./_read-input.js').readJsonOrRefuse(file, 'the PRD', { expect: 'object' });
38
+ } catch (err) {
39
+ // LOUD. A silent failure here is indistinguishable from a PRD with no phases, and the caller
40
+ // treats that as "nothing to do" rather than "I could not tell".
41
+ process.stderr.write(`[prd-phases] cannot read ${file}: ${err.message}\n`);
42
+ process.exit(1);
43
+ }
44
+
45
+ const phases = Object.keys(prd.implementationOrder || {});
46
+ process.stdout.write(phases.length ? `${phases.join('\n')}\n` : '');
@@ -0,0 +1,7 @@
1
+ const fs = require('fs');
2
+ const paths = (process.env.COMPLETED_LIST||'').split(':').filter(Boolean);
3
+ const stories = paths.flatMap(p => {
4
+ try { return JSON.parse(fs.readFileSync(p,'utf8')).stories||[]; } catch { return []; }
5
+ });
6
+ fs.writeFileSync(process.argv[2], JSON.stringify({stories},null,2));
7
+
@@ -0,0 +1,23 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * record-declared-pass.js <prd> <codeline> — a lane the operator declared passed (EPAM_PASSED_LANES) is that
4
+ * codeline's result for every story spanning it, recorded as one and marked as declared. A codeline that already
5
+ * has a result keeps it. AMSD-2974 run 9 (2026-10-08): declared lanes recorded nothing, and the completeness check
6
+ * (spanning-stories-incomplete.js) failed the run for them.
7
+ */
8
+ 'use strict';
9
+ const fs = require('fs');
10
+
11
+ const [file, cl] = process.argv.slice(2);
12
+ try {
13
+ const prd = JSON.parse(fs.readFileSync(file, 'utf8'));
14
+ let changed = false;
15
+ for (const s of prd.stories || []) {
16
+ if (!s || !Array.isArray(s.codelines) || !s.codelines.includes(cl)) continue;
17
+ s.perCodeline = s.perCodeline || {};
18
+ if (s.perCodeline[cl]) continue;
19
+ s.perCodeline[cl] = { status: 'completed', completed: true, completedAt: new Date().toISOString(), reviewStatus: 'declared-passed' };
20
+ changed = true;
21
+ }
22
+ if (changed) fs.writeFileSync(file, JSON.stringify(prd, null, 2));
23
+ } catch (e) { process.stderr.write(`[record-declared-pass] ${e.message}\n`); process.exit(1); }
@@ -0,0 +1,31 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * release-line-prd.js <lane-prd> <out> <codeline> <line> — the lane's PRD for one more release line.
4
+ *
5
+ * The stories whose plan ships on that line for this codeline, pending again — a story completed on the codeline's
6
+ * own base is not done on another line (each line is its own branch and its own change). Everything else about the
7
+ * story (its plan, its fix sites, its criteria) is kept: the line is worked from the same plan.
8
+ */
9
+ 'use strict';
10
+ const fs = require('fs');
11
+
12
+ const [src, out, codeline, line, canonicalFile] = process.argv.slice(2);
13
+ const prd = JSON.parse(fs.readFileSync(src, 'utf8'));
14
+ // A resumed lane works from its checkpoint PRD, which may predate the plan: the canonical PRD holds it then.
15
+ const canonical = canonicalFile && fs.existsSync(canonicalFile) ? JSON.parse(fs.readFileSync(canonicalFile, 'utf8')) : null;
16
+ for (const s of prd.stories || []) {
17
+ const c = s && !s.overallPlan && ((canonical && canonical.stories) || []).find((x) => x && x.id === s.id);
18
+ if (c && c.overallPlan) s.overallPlan = c.overallPlan;
19
+ }
20
+ const onLine = (s) => !!(s && s.overallPlan && (s.overallPlan.packages || []).some((p) => p && p.providedBy === codeline
21
+ && (p.releaseLines || []).some((l) => l && l.line === line)));
22
+ const stories = (prd.stories || []).filter(onLine).map((s) => ({
23
+ ...s, status: 'pending', completed: false, passes: false, completedAt: null,
24
+ }));
25
+ const ids = new Set(stories.map((s) => s.id));
26
+ const order = {};
27
+ for (const [phase, list] of Object.entries(prd.implementationOrder || {})) {
28
+ const kept = (list || []).filter((id) => ids.has(id));
29
+ if (kept.length) order[phase] = kept;
30
+ }
31
+ fs.writeFileSync(out, JSON.stringify({ ...prd, stories, implementationOrder: order }, null, 2));
@@ -0,0 +1,31 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * release-lines.js <prd> <codeline> — the release lines this codeline ships on beyond its own base.
4
+ *
5
+ * Read from the story's plan (detective-overall-plan): for each package this codeline provides, each release line
6
+ * the plan marks onCodelineBase:false, as "<line>\t<base>". The plan decides how many — 0, 1 or N; a line whose
7
+ * onCodelineBase is not stated is not guessed at. A codeline used to have one base and one story branch, and a shared
8
+ * library could not ship on the older lines its consumers are locked to (AMSD-2974, 2026-10-06).
9
+ */
10
+ 'use strict';
11
+ const fs = require('fs');
12
+
13
+ const [prdFile, codeline, canonicalFile] = process.argv.slice(2);
14
+ const prd = JSON.parse(fs.readFileSync(prdFile, 'utf8'));
15
+ // A resumed lane works from its checkpoint PRD, which may predate the plan: the canonical PRD holds it then.
16
+ const canonical = canonicalFile && fs.existsSync(canonicalFile) ? JSON.parse(fs.readFileSync(canonicalFile, 'utf8')) : null;
17
+ const planOf = (s) => (s && s.overallPlan) || ((canonical && canonical.stories || []).find((c) => c && c.id === s.id) || {}).overallPlan;
18
+ const seen = new Set();
19
+ for (const s of (prd && Array.isArray(prd.stories) ? prd.stories : [])) {
20
+ const plan = s && planOf(s);
21
+ for (const p of (plan && Array.isArray(plan.packages) ? plan.packages : [])) {
22
+ if (!p || p.providedBy !== codeline) continue;
23
+ for (const l of (Array.isArray(p.releaseLines) ? p.releaseLines : [])) {
24
+ if (!l || l.onCodelineBase !== false || !l.line || !l.base) continue;
25
+ const key = `${l.line}\t${l.base}`;
26
+ if (seen.has(key)) continue;
27
+ seen.add(key);
28
+ process.stdout.write(`${key}\n`);
29
+ }
30
+ }
31
+ }
@@ -0,0 +1,108 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * WHAT IS NEVER AGENT OUTPUT — one answer, for every caller that needs it.
4
+ *
5
+ * Two shell files each carried a hand-written list, and they had drifted. worktree-health-check.sh
6
+ * named `.dart_tool`, `build`, `node_modules`; lib/git-ops.sh named `node_modules`, `build`,
7
+ * `.next`. Between them they described one ecosystem.
8
+ *
9
+ * The consequences ran in both directions. `git_add_client_outputs` decides what is COMMITTED TO A
10
+ * CLIENT REPOSITORY: on a Rust codeline whose target/ was not gitignored, the build tree was staged
11
+ * into the customer's repo. And the health check's list decides what counts as the agent's
12
+ * uncommitted work: the same directory was reported as thousands of uncommitted files, which set
13
+ * issues=1, which made Step 3.1 exit 1 and killed the phase.
14
+ *
15
+ * Ecosystem artefacts come from lib/ecosystem-registry.js, beside the ecosystem that produces them. Editor
16
+ * and OS droppings come from config/repo-artifacts.json. Nothing is named here.
17
+ *
18
+ * argv[2] the form the caller wants:
19
+ * pathspec git exclude pathspecs, one per line — for `git add -A -- ...`
20
+ * glob shell case-patterns, one per line — for a status-line filter
21
+ * regex one ERE matching any excluded path — for a `grep -vE` filter
22
+ * diff pathspec PLUS lockfiles — for the patch a REVIEW agent is shown
23
+ *
24
+ * `diff` is deliberately wider than `pathspec`. A lockfile must still be STAGED: a real dependency
25
+ * change belongs in the commit. But it must not be REVIEWED — it is machine-generated, frequently
26
+ * thousands of lines, and it would consume the reviewer's whole budget before it reached the code
27
+ * the change is actually about. Staging and reviewing are different questions, so they get
28
+ * different answers rather than one list bent to serve both.
29
+ *
30
+ * Every directory is emitted in BOTH the top-level and the nested form. `:!*​/node_modules/*`
31
+ * matches only a NESTED one; a top-level node_modules — the usual case — needs `:!node_modules/*`.
32
+ * The original list carried only the nested form, so a repo without node_modules in .gitignore
33
+ * staged the lot.
34
+ *
35
+ * Exit non-zero if the registry or the config cannot be read. A caller that silently fell back to
36
+ * an empty exclusion list would stage everything, which is the failure this exists to prevent.
37
+ */
38
+ 'use strict';
39
+
40
+ const fs = require('fs');
41
+ const path = require('path');
42
+
43
+ const { allArtifactDirs, allManifests } = require('../ecosystem-registry.js');
44
+
45
+ const form = process.argv[2] || 'pathspec';
46
+ if (!['pathspec', 'glob', 'regex', 'diff'].includes(form)) {
47
+ process.stderr.write(`[repo-exclude-patterns] unknown form: ${form}\n`);
48
+ process.exit(2);
49
+ }
50
+
51
+ const configPath = path.join(__dirname, '..', '..', '..', 'config', 'repo-artifacts.json');
52
+ let config;
53
+ try {
54
+ config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
55
+ } catch (err) {
56
+ // LOUD. An empty exclusion list stages a client's build tree into their repository.
57
+ process.stderr.write(`[repo-exclude-patterns] cannot read ${configPath}: ${err.message}\n`);
58
+ process.exit(1);
59
+ }
60
+
61
+ const dirs = [...new Set([...allArtifactDirs(), ...(config.dirs || [])])];
62
+ const files = config.files || [];
63
+ const enginePaths = config.enginePaths || [];
64
+
65
+ // Lockfile names come from the ecosystem registry, beside the ecosystem that produces them.
66
+ const lockfiles = [...new Set(allManifests().flatMap((e) => Object.keys(e.lockfiles || {})))];
67
+
68
+ const out = [];
69
+ if (form === 'pathspec' || form === 'diff') {
70
+ // THE LONG FORM, ALWAYS. git parses `:!` as the start of a magic signature and keeps reading
71
+ // magic characters, so `:!__pycache__/*` dies with "Unimplemented pathspec magic '_'" — which
72
+ // fails the whole `git add` and stages NOTHING. `:(exclude)` is unambiguous whatever follows.
73
+ // ONE GLOB PER DIRECTORY, NOT A TOP-LEVEL AND A NESTED PREFIX.
74
+ //
75
+ // `:(exclude)<dir>/*` is FATAL, not merely unmatched, when the directory is a symlink — git
76
+ // refuses a pathspec that crosses one:
77
+ //
78
+ // fatal: pathspec ':(exclude)node_modules/*' is beyond a symbolic link
79
+ //
80
+ // Live 2026-08-18, both lanes, at the LAST step of a run that had otherwise succeeded — the fix
81
+ // was correct, the tests passed, the type check passed — every commit failed with exit 128
82
+ // because both codelines symlink node_modules to a shared install. Nothing could ever be
83
+ // committed in such a repo, and the story was demoted as undelivered.
84
+ //
85
+ // The bare `:(exclude)<dir>` form survives the symlink but stops excluding NESTED copies, which
86
+ // would stage a client's vendored tree: fixing the error by weakening the exclusion. The glob
87
+ // form is correct on all three counts — top-level contents, nested contents, and a symlinked
88
+ // directory — and replaces both prefixes with one pattern.
89
+ // TWO PATTERNS: the CONTENTS at any depth, and the ENTRY itself. A symlinked directory is a
90
+ // single entry to git, so the contents glob does not cover it and the link would be staged.
91
+ for (const d of dirs) out.push(`:(exclude,glob)**/${d}/**`, `:(exclude,glob)**/${d}`);
92
+ for (const f of files) out.push(`:(exclude)${f}`, `:(exclude)*/${f}`);
93
+ for (const p of enginePaths) out.push(`:(exclude)${p}`);
94
+ if (form === 'diff') {
95
+ for (const f of lockfiles) out.push(`:(exclude)${f}`, `:(exclude)*/${f}`);
96
+ }
97
+ } else if (form === 'regex') {
98
+ // Anchored at the start OR after a slash, so it matches a top-level and a nested one alike.
99
+ const alt = [...dirs, ...files].map((d) => d.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')).join('|');
100
+ process.stdout.write(`(^|/)(${alt})(/|$)\n`);
101
+ process.exit(0);
102
+ } else {
103
+ for (const d of dirs) out.push(`${d}/*`, `*/${d}/*`);
104
+ for (const f of files) out.push(f, `*/${f}`);
105
+ for (const p of enginePaths) out.push(p);
106
+ }
107
+
108
+ process.stdout.write(`${out.join('\n')}\n`);
@@ -0,0 +1,16 @@
1
+ // THE PRD IS argv[2]. This read argv[1] — the script's own path — so it parsed its own
2
+ // JavaScript as JSON, threw, and answered "no spec output" for every PRD ever passed to it.
3
+ // The guard could never pass, so every resume that skips the spec pass was refused. Same
4
+ // extraction defect as lib/handlers/prd-phases.js, which carried a literal '$1' from the
5
+ // shell heredoc it was lifted out of and made every lane loop zero times.
6
+ try {
7
+ const p = JSON.parse(require('fs').readFileSync(process.argv[2], 'utf8'));
8
+ const stories = Array.isArray(p.stories) ? p.stories : [];
9
+ const has = (v) => Array.isArray(v) ? v.length > 0
10
+ : (v && typeof v === 'object') ? Object.keys(v).length > 0 : false;
11
+ const survived = stories.some((s) => s && (
12
+ has(s.fixSiteAnalysis) || has(s.verificationCriteria) ||
13
+ has(s.specification) || has((s.technicalNotes || {}).files)));
14
+ process.exit(survived ? 0 : 1);
15
+ } catch { process.exit(1); }
16
+
@@ -0,0 +1,43 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * WHICH OF THIS STORY'S OTHER CODELINES PROVIDE A PACKAGE THIS CODELINE DECLARES.
4
+ *
5
+ * A story spanning a shared library and the sites that consume it changes the library in its own
6
+ * lane; a site lane must build and test against THAT change, not the release it has installed. The
7
+ * facts come from lib/codeline-packages.js (each codeline's manifest, read by its ecosystem
8
+ * provider), scoped to the codelines the story spans — never the whole estate, never configured.
9
+ *
10
+ * argv[2] the lane's PRD (project.outputDirs: [{codeline, path}])
11
+ * argv[3] this codeline's root
12
+ * stdout one "packageName<TAB>providerRoot" line per sibling package this codeline declares
13
+ */
14
+ 'use strict';
15
+ const fs = require('fs');
16
+ const path = require('path');
17
+ const { codelinePackages } = require('../codeline-packages.js');
18
+
19
+ try {
20
+ const prd = JSON.parse(fs.readFileSync(process.argv[2], 'utf8'));
21
+ const here = path.resolve(process.argv[3] || '');
22
+ const dirs = (prd.project && Array.isArray(prd.project.outputDirs)) ? prd.project.outputDirs : [];
23
+ const mine = dirs.find((d) => d && d.path && path.resolve(d.path) === here);
24
+ if (!mine) process.exit(0);
25
+ const facts = codelinePackages(prd, dirs.map((d) => d.codeline));
26
+ const me = facts.find((f) => f.codeline === mine.codeline);
27
+ for (const name of Object.keys((me && me.declares) || {})) {
28
+ const provider = facts.find((f) => f.codeline !== mine.codeline && f.provides && f.provides.name === name);
29
+ const root = provider && (dirs.find((d) => d.codeline === provider.codeline) || {}).path;
30
+ // THE LINE THIS CONSUMER RECEIVES (the story's plan): a version shipped on a release line off the provider's own
31
+ // base is built from that line's story branch — suffix and story id, for the caller to name it.
32
+ let suffix = ''; let story = '';
33
+ for (const s of prd.stories || []) {
34
+ for (const p of ((s && s.overallPlan && s.overallPlan.packages) || [])) {
35
+ if (p.package !== name) continue;
36
+ const c = (p.consumers || []).find((x) => x && x.codeline === mine.codeline);
37
+ const l = c && (p.releaseLines || []).find((x) => x && x.version === c.receivesVia);
38
+ if (l && l.onCodelineBase === false && !suffix) { suffix = `-${String(l.line).replace(/[^A-Za-z0-9._-]/g, '-')}`; story = s.id; }
39
+ }
40
+ }
41
+ if (root) process.stdout.write(`${name}\t${root}\t${suffix}\t${story}\n`);
42
+ }
43
+ } catch { process.exit(0); }
@@ -0,0 +1,50 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * WHICH SPANNING STORIES HAVE NOT REPORTED FROM EVERY CODELINE.
4
+ *
5
+ * A story listing two or more codelines is not finished when one lane succeeds — it is finished
6
+ * when every codeline it names has produced a result. Running it as "done" after one lane reports
7
+ * ships a fraction of the work while the run says success.
8
+ *
9
+ * Lifted out of run-agent-orchestration.sh on 2026-08-16, where it was a `node -e "..."` string.
10
+ * As a string it could not be tested, linted or type-checked, and it was invoked with
11
+ * `2>/dev/null || true`, so a syntax error in it and "every story is complete" were the same
12
+ * observation.
13
+ *
14
+ * Generic: the PRD path is an argument, and the rule holds for any project and any stack.
15
+ *
16
+ * argv[2] path to the PRD
17
+ * stdout "<story-id> → no result for: <codeline>, <codeline>" per offender, "; "-separated
18
+ * exit 0 always when the PRD could be read — an empty result means nothing is incomplete
19
+ * exit 1 the PRD is missing or unreadable, which is NOT the same as nothing being incomplete
20
+ */
21
+ 'use strict';
22
+
23
+ const fs = require('fs');
24
+
25
+ const prdPath = process.argv[2];
26
+ if (!prdPath) {
27
+ process.stderr.write('[spanning-stories-incomplete] usage: <prd-path>\n');
28
+ process.exit(1);
29
+ }
30
+
31
+ let prd;
32
+ try {
33
+ prd = require('./_read-input.js').readJsonOrRefuse(prdPath, 'the PRD', { expect: 'object' });
34
+ } catch (e) {
35
+ // LOUD. The inline version swallowed this: an unreadable PRD produced an empty string, which
36
+ // the caller read as "no spanning story is incomplete" and carried on.
37
+ process.stderr.write(`[spanning-stories-incomplete] cannot read ${prdPath}: ${e && e.message}\n`);
38
+ process.exit(1);
39
+ }
40
+
41
+ const bad = [];
42
+ for (const s of (prd.stories || [])) {
43
+ const want = Array.isArray(s.codelines) ? s.codelines : [];
44
+ if (want.length < 2) continue; // not a spanning story
45
+ const got = s.perCodeline || {};
46
+ const missing = want.filter((cl) => !got[cl]);
47
+ if (missing.length) bad.push(`${s.id} → no result for: ${missing.join(', ')}`);
48
+ }
49
+
50
+ process.stdout.write(bad.join('; '));
@@ -0,0 +1,48 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * spec-inputs-digest.js [orchestrationsDir] — WHAT A LANE'S SPECIFICATION WAS BUILT FROM, as one digest.
4
+ *
5
+ * config/spec-inputs.json declares it: the seams that produce the specification (by what they
6
+ * PRODUCE in the registry, never by name) — each one's base SI and every template file of its
7
+ * template family — and the SI contract's sections every agent receives. A lane's pre-writer
8
+ * checkpoint records this; a resume compares it (lib/run-checkpoint.sh lane_spec_is_current).
9
+ *
10
+ * Prints a sha256 hex digest; nothing when the declaration cannot be read.
11
+ */
12
+ 'use strict';
13
+ const fs = require('fs');
14
+ const path = require('path');
15
+ const crypto = require('crypto');
16
+
17
+ function specInputsDigest(orchDir) {
18
+ const decl = JSON.parse(fs.readFileSync(path.join(orchDir, 'config', 'spec-inputs.json'), 'utf8'));
19
+ const reg = JSON.parse(fs.readFileSync(path.join(orchDir, 'agents', 'invocation-profiles.json'), 'utf8'));
20
+ const contract = JSON.parse(fs.readFileSync(path.join(orchDir, 'config', 'agent-contract.json'), 'utf8'));
21
+ const tplDir = path.join(orchDir, 'prompts', 'templates');
22
+ const tplFiles = fs.readdirSync(tplDir).filter((f) => f.endsWith('.json')).sort();
23
+ const produces = new Set(decl.produces || []);
24
+ const parts = [];
25
+ for (const [seam, p] of Object.entries(reg.profiles || {}).sort(([a], [b]) => a.localeCompare(b))) {
26
+ if (!p || !produces.has(p.produces)) continue;
27
+ parts.push(['seam', seam, p.si || '']);
28
+ const t = p.template;
29
+ if (!t) continue;
30
+ // The template and its family (<template>.<variant>.json, <template>-<part>.json).
31
+ for (const f of tplFiles) {
32
+ const id = f.slice(0, -5);
33
+ if (id === t || id.startsWith(`${t}.`) || id.startsWith(`${t}-`)) parts.push(['template', f, fs.readFileSync(path.join(tplDir, f), 'utf8')]);
34
+ }
35
+ }
36
+ // Templates the spec seams render without owning them (config: templates).
37
+ for (const t of decl.templates || []) {
38
+ for (const f of tplFiles) if (f.slice(0, -5) === t) parts.push(['shared-template', f, fs.readFileSync(path.join(tplDir, f), 'utf8')]);
39
+ }
40
+ for (const k of decl.contractSections || []) parts.push(['contract', k, contract[k] === undefined ? null : contract[k]]);
41
+ return crypto.createHash('sha256').update(JSON.stringify(parts)).digest('hex');
42
+ }
43
+
44
+ if (require.main === module) {
45
+ try { process.stdout.write(specInputsDigest(process.argv[2] || path.join(__dirname, '..', '..', '..'))); } catch { process.exit(0); }
46
+ }
47
+
48
+ module.exports = { specInputsDigest };
@@ -0,0 +1,40 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * spec-predates-plan.js <prd> [canonical-prd] — the specified stories whose spec was built under another plan.
4
+ *
5
+ * A story's spec (fix sites, criteria) is the elaboration of its plan. AMSD-2974 (2026-10-08): every lane resumed a
6
+ * spec approved on 10-01, before the story had a plan; the plan then gave upexpress no change while its criteria
7
+ * demanded upexpress-owned work — the reviewer rejected, the writer was skipped, round after round. A spec made under
8
+ * a different plan, or under none, is the spec of another story: it is specified again, from the plan it has now.
9
+ *
10
+ * stdout one story id per line whose spec predates its plan; nothing otherwise. The plan is the story's own, or
11
+ * the canonical PRD's when the story carries none (a lane's checkpoint copy may predate the plan).
12
+ */
13
+ 'use strict';
14
+ const fs = require('fs');
15
+ const path = require('path');
16
+ const { planHash } = require(path.join(__dirname, '..', 'plan-hash.js'));
17
+
18
+ function specPredatesPlan(prd, canonical) {
19
+ const canon = new Map(((canonical && canonical.stories) || []).filter(Boolean).map((s) => [s.id, s]));
20
+ return (prd.stories || [])
21
+ .filter((s) => s && s.status !== 'deprecated' && s.specification && s.specification.runId)
22
+ .filter((s) => {
23
+ // The run's plan is the canonical PRD's; a lane copy carries the plan it was specified under.
24
+ const plan = (canon.get(s.id) || {}).overallPlan || s.overallPlan;
25
+ const now = planHash(plan);
26
+ return now !== '' && now !== (s.specification.planHash || '');
27
+ })
28
+ .map((s) => s.id);
29
+ }
30
+
31
+ module.exports = { specPredatesPlan };
32
+
33
+ if (require.main === module) {
34
+ try {
35
+ const prd = JSON.parse(fs.readFileSync(process.argv[2], 'utf8'));
36
+ const canonical = process.argv[3] && fs.existsSync(process.argv[3]) ? JSON.parse(fs.readFileSync(process.argv[3], 'utf8')) : null;
37
+ const out = specPredatesPlan(prd, canonical);
38
+ if (out.length) process.stdout.write(out.join('\n') + '\n');
39
+ } catch (e) { process.stderr.write(`[spec-predates-plan] ${e.message}\n`); }
40
+ }
@@ -0,0 +1,64 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * WHICH PENDING STORIES OF A PHASE CARRY NO SPEC OUTPUT?
4
+ *
5
+ * The spec pass elaborates a story and records it under specification.appliedAgents (the same
6
+ * mark spec-mode-runner uses to count a story as specified). On a resume the run mode turns the
7
+ * whole spec pass off, which is right only while EVERY pending story was elaborated; a story
8
+ * whose content the remediation restored from canonical (its elaboration had gone to a
9
+ * placeholder child, regintel run 20260915T101555Z) reached the writer with no spec. This names
10
+ * the stories that still need the pass, so the resume runs it for them and them alone.
11
+ *
12
+ * Generic: the PRD is an argument; nothing here names a project, a stack or a story.
13
+ *
14
+ * argv[1] the PRD
15
+ * argv[2] a phase (optional; every phase when absent)
16
+ * stdout the story ids, one per line; nothing when every pending story is specified
17
+ */
18
+ const fs = require('fs');
19
+
20
+ /**
21
+ * A STORY THAT SPANS CODELINES IS SPECIFIED PER CODELINE. In a lane's PRD (story.codeline = the lane)
22
+ * the spec mark from another lane is not this lane's spec: AMSD-2974's three site lanes skipped their
23
+ * spec pass on a resume because cxshared's spec had stamped the story (2026-10-01). The lane's own
24
+ * fix-site entry is the mark that this codeline was investigated.
25
+ */
26
+ function lacksOwnLane(s) {
27
+ if (!Array.isArray(s.codelines) || s.codelines.length < 2 || !s.codeline) return false;
28
+ // This lane's own spec pass stamped its codeline on the spec (spec-mode-runner.js stampSpecification).
29
+ if (s.specification && s.specification.runId && s.specification.codeline === s.codeline) return false;
30
+ const own = s.fixSiteAnalysisPerCodeline;
31
+ return !(own && Object.prototype.hasOwnProperty.call(own, s.codeline));
32
+ }
33
+
34
+ // A spec built under another plan, or before the story had one, is not this story's spec (spec-predates-plan.js).
35
+ const { specPredatesPlan } = require('./spec-predates-plan.js');
36
+
37
+ function storiesLackingSpec(prd, phase) {
38
+ const predates = new Set(specPredatesPlan(prd));
39
+ const byId = new Map((prd.stories || []).map((s) => [s.id, s]));
40
+ const ids = phase
41
+ ? ((prd.implementationOrder || {})[phase] || [])
42
+ : Object.values(prd.implementationOrder || {}).flat();
43
+ return ids
44
+ .map((id) => byId.get(id))
45
+ .filter((s) => s && s.completed !== true && s.status !== 'deprecated')
46
+ // THE PASS'S WRITE IS THE MARK. Every spec-pass write stamps specification.runId — on the
47
+ // story it specified and on each child it split off. Judging by appliedAgents counted the
48
+ // pass's own children as spec-less, and every resume re-split them (run 20260915T101555Z).
49
+ .filter((s) => !(s.specification && s.specification.runId) || lacksOwnLane(s) || predates.has(s.id))
50
+ .map((s) => s.id);
51
+ }
52
+
53
+ module.exports = { storiesLackingSpec };
54
+
55
+ if (require.main === module) {
56
+ try {
57
+ const prd = JSON.parse(fs.readFileSync(process.argv[2], 'utf8'));
58
+ const out = storiesLackingSpec(prd, process.argv[3] || '');
59
+ if (out.length) process.stdout.write(out.join('\n') + '\n');
60
+ } catch (e) {
61
+ process.stderr.write(`[stories-lacking-spec] ${e.message}\n`);
62
+ process.exit(2);
63
+ }
64
+ }
@@ -0,0 +1,92 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * write-missing-plans.js <canonical-prd> — a story specified before it had a plan gets its plan.
4
+ *
5
+ * The story's plan (detective-overall-plan) is written by its first detective during the spec pass. A story already
6
+ * specified before plans existed is never specified again on a resume, so no detective writes one and no release line
7
+ * the plan would name can ever run (the £0 rehearsal of AMSD-2974, 2026-10-06). Operator, 2026-10-06: such a story
8
+ * gets its plan — the detective's plan only: its fix sites, criteria and every other field are left as they are.
9
+ *
10
+ * Which stories: every story with no plan — the plan comes first, before any lane, so every codeline and every release
11
+ * line is worked from it (a resume's canonical PRD may carry no spec output at all: it lives in the lanes' checkpoints).
12
+ * A story with a plan keeps it. The detective runs as the spec pass runs it, for the story's first codeline, able to
13
+ * read every codeline of the story. A detective that cannot answer leaves no plan and says so — the run goes on.
14
+ */
15
+ 'use strict';
16
+ const fs = require('fs');
17
+ const path = require('path');
18
+
19
+ const prdFile = process.argv[2];
20
+ const prd = JSON.parse(fs.readFileSync(prdFile, 'utf8'));
21
+ const dirs = (prd.project && Array.isArray(prd.project.outputDirs)) ? prd.project.outputDirs : [];
22
+ const { planScope, adoptPlanCodelines } = require('../plan-scope.js');
23
+
24
+ // A STORY IS PLANNED ONCE PER RUN. A resume restores the canonical PRD from the run's checkpoint, which never held the
25
+ // plan; asked again, the detective answered differently (AMSD-2974, 2026-10-07). The plan a run wrote is kept under the
26
+ // run and is the plan on every later launch of it.
27
+ const cfg = process.env.EPAM_PROJECT_CONFIG_DIR || process.env.PROJECT_CONFIG_DIR || '';
28
+ const run = process.env.EPAM_RESUME_RUN || process.env.ORCH_RUN_ID || '';
29
+ // What discovery grounded this run (one top-level key per codeline, with its path); absent = the story's own codelines.
30
+ let facts = null;
31
+ try { facts = cfg ? JSON.parse(fs.readFileSync(path.join(cfg, 'codeline-facts.json'), 'utf8')) : null; } catch { facts = null; }
32
+ const keptDir = cfg && run ? path.join(cfg, 'runs', run, 'plans') : '';
33
+ const keptFile = (id) => (keptDir ? path.join(keptDir, `${String(id).replace(/[^A-Za-z0-9._-]/g, '_')}.json`) : '');
34
+ const keep = (s) => {
35
+ const f = keptFile(s.id);
36
+ if (!f || fs.existsSync(f)) return;
37
+ fs.mkdirSync(keptDir, { recursive: true });
38
+ fs.writeFileSync(f, JSON.stringify(s.overallPlan, null, 2));
39
+ };
40
+ let restored = false;
41
+ for (const s of prd.stories || []) {
42
+ if (!s) continue;
43
+ if (s.overallPlan) { keep(s); if (adoptPlanCodelines(s, prd, facts).length) restored = true; continue; }
44
+ const f = keptFile(s.id);
45
+ if (f && fs.existsSync(f)) {
46
+ s.overallPlan = JSON.parse(fs.readFileSync(f, 'utf8'));
47
+ adoptPlanCodelines(s, prd, facts);
48
+ restored = true;
49
+ process.stderr.write(`[plan] ${s.id}: the plan this run wrote is kept — ${f}\n`);
50
+ }
51
+ }
52
+ if (restored) fs.writeFileSync(prdFile, JSON.stringify(prd, null, 2));
53
+ const wanting = (prd.stories || []).filter((s) => s && !s.overallPlan);
54
+ if (!wanting.length) process.exit(0);
55
+
56
+ process.env.SPEC_MODE_NO_MAIN = '1';
57
+ // eslint-disable-next-line global-require
58
+ const runner = require(path.join(__dirname, '..', '..', 'spec-mode-runner.js'));
59
+
60
+ (async () => {
61
+ let changed = false;
62
+ for (const s of wanting) {
63
+ const first = (Array.isArray(s.codelines) && s.codelines[0]) || s.codeline;
64
+ const root = (dirs.find((d) => d && d.codeline === first) || {}).path || prd.project.outputDir || '';
65
+ // THE DETECTIVE IS SHOWN EVERY CODELINE DISCOVERY GROUNDED (lib/plan-scope.js), not only the router's pick: the plan
66
+ // decides which codelines change and which only receive (AMSD-2974 run 1, 2026-10-09).
67
+ const scope = planScope(s, prd, facts);
68
+ const probe = { ...s, codeline: first, codelines: scope.codelines };
69
+ delete probe.overallPlan;
70
+ const prevRoot = process.env.PROJECT_ROOT;
71
+ process.env.PROJECT_ROOT = root;
72
+ try {
73
+ await runner.runCodeGraphDetective(probe, process.env.LOG_DIR || '', { prd: { ...prd, project: { ...prd.project, outputDirs: scope.outputDirs } } });
74
+ } catch (e) {
75
+ process.stderr.write(`[plan] ${s.id}: the detective did not answer (${(e && e.message) || e})\n`);
76
+ } finally {
77
+ if (prevRoot === undefined) delete process.env.PROJECT_ROOT; else process.env.PROJECT_ROOT = prevRoot;
78
+ }
79
+ if (probe.overallPlan) {
80
+ s.overallPlan = probe.overallPlan;
81
+ keep(s);
82
+ changed = true;
83
+ const added = adoptPlanCodelines(s, prd, facts);
84
+ if (added.length) process.stderr.write(`[plan] ${s.id}: the plan works ${added.join(', ')} too — the story is worked in ${s.codelines.join(', ')}\n`);
85
+ process.stderr.write(`[plan] ${s.id}: plan written (${probe.overallPlan.codelines.length} codeline(s), `
86
+ + `${(probe.overallPlan.packages || []).reduce((n, p) => n + (p.releaseLines || []).length, 0)} release line(s))\n`);
87
+ } else {
88
+ process.stderr.write(`[plan] ${s.id}: no plan — the detective gave none; this story's release lines cannot be known\n`);
89
+ }
90
+ }
91
+ if (changed) fs.writeFileSync(prdFile, JSON.stringify(prd, null, 2));
92
+ })().catch((e) => { process.stderr.write(`[plan] could not write plans: ${(e && e.stack) || e}\n`); process.exit(0); });