amsd-pipeline-workflow 0.1.23 → 0.1.25
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.
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
// THE SIBLING PROVIDER IS BUILT BEFORE THE WRITER, OUTSIDE ITS MEMORY SCOPE (run 11, 2026-10-21): a consumer's Step 19 type-checks it against the provider's own build, and the pipeline builds that provider as a hook INSIDE the
|
|
3
|
+
// Writer session — cx-shared's build is 3.2 GB (anon) and 67 s, and the session's scope is 4096 MB: the kernel killed it twice. The pipeline's own provisioning (provision_lane_worktree → link_sibling_builds) builds once per
|
|
4
|
+
// provider checkout and stamps it in LOG_DIR; this runs that same provisioning from the driver — in the leg's scope — on a throwaway worktree of the consumer, with LOG_DIR = the lane's persistent log, so the stamp survives
|
|
5
|
+
// and the generator seeds it into the Writer's state (generator/sibling-stamps.js). Nothing is re-implemented: the handler that finds the providers and the function that builds them are the pipeline's.
|
|
6
|
+
// prebuildSiblings(env, { root, laneLog, prd, log? }) → { providers: n } a build that fails is reported by the pipeline's function and left unstamped (tried again next time); nothing here throws
|
|
7
|
+
const fs = require('fs');
|
|
8
|
+
const os = require('os');
|
|
9
|
+
const path = require('path');
|
|
10
|
+
const { spawnSync } = require('child_process');
|
|
11
|
+
const { bash, handler } = require('./pipeline.js');
|
|
12
|
+
|
|
13
|
+
const git = (repo, ...a) => spawnSync('git', ['-C', repo, ...a], { encoding: 'utf8' });
|
|
14
|
+
|
|
15
|
+
function prebuildSiblings(env, { root, laneLog, prd, log }) {
|
|
16
|
+
const out = handler(env, 'sibling-providers', [prd, root]);
|
|
17
|
+
const providers = out.stdout.split('\n').filter(Boolean);
|
|
18
|
+
if (!providers.length) return { providers: 0 };
|
|
19
|
+
fs.mkdirSync(laneLog, { recursive: true });
|
|
20
|
+
const tmp = fs.mkdtempSync(path.join(env.TMPDIR || os.tmpdir(), 'receiving-')); const wt = path.join(tmp, path.basename(root));
|
|
21
|
+
try {
|
|
22
|
+
const add = git(root, 'worktree', 'add', '-q', '--detach', wt, 'HEAD');
|
|
23
|
+
if (add.status !== 0) { if (log) log(`[sibling] prebuild: could not make a worktree of ${root}: ${(add.stderr || '').trim()}`); return { providers: providers.length, error: 'worktree' }; }
|
|
24
|
+
if (log) log(`[sibling] prebuild: ${providers.map((l) => l.split('\t')[0]).join(', ')} for ${path.basename(root)} — built here, before the Writer, so the Writer's session never builds it`);
|
|
25
|
+
bash({ ...env, LOG_DIR: laneLog, PRD_FILE: prd, MAIN_PRD_FILE: prd }, 'provision_lane_worktree', [root, wt], { log });
|
|
26
|
+
} finally {
|
|
27
|
+
git(root, 'worktree', 'remove', '--force', wt); fs.rmSync(tmp, { recursive: true, force: true }); git(root, 'worktree', 'prune');
|
|
28
|
+
}
|
|
29
|
+
return { providers: providers.length };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
module.exports = { prebuildSiblings };
|
package/driver/stages.js
CHANGED
|
@@ -7,6 +7,7 @@ const path = require('path');
|
|
|
7
7
|
const { spawnSync } = require('child_process');
|
|
8
8
|
const runtime = require('./runtime.js');
|
|
9
9
|
const { bash } = require('./pipeline.js');
|
|
10
|
+
const { prebuildSiblings } = require('./sibling-prebuild.js');
|
|
10
11
|
|
|
11
12
|
// THE NAME EACH WORKFLOW IS RUN BY (and so the file it is placed in, <codeline>/.claude/workflows/<name>.js). No name may match a test runner's default pattern: the Spec workflow was "spec", the client's jest ran
|
|
12
13
|
// spec.js as a test, and every whole-suite run in a codeline reported the pipeline's own file as a failing suite (run 11, 2026-10-20; test/integration/workflow-placement.test.js).
|
|
@@ -213,6 +214,9 @@ function overlay(ctx, lane, lineSlug) {
|
|
|
213
214
|
|
|
214
215
|
async function runWriter(ctx, lane, env, { story, out, lineSlug, reviewCommitted }) {
|
|
215
216
|
fs.rmSync(out, { recursive: true, force: true });
|
|
217
|
+
// A CODELINE THAT RECEIVES ANOTHER'S CHANGE has the provider built BEFORE the Writer, here, outside the session's memory scope (driver/sibling-prebuild.js; run 11's two OOM kills). Never blocks the Writer: said, and the
|
|
218
|
+
// session's own Step 19 would then try the build itself, as before.
|
|
219
|
+
try { prebuildSiblings(env, { root: lane.wt, laneLog: env.LOG_DIR, prd: env.PRD_FILE || env.MAIN_PRD_FILE, log: ctx.log }); } catch (e) { ctx.log(`[sibling] prebuild failed — ${e.message}`); }
|
|
216
220
|
const inst = overlay(ctx, lane, lineSlug);
|
|
217
221
|
// the run's recorded launch settings (driver/launch-env.js) go to the generator as its --launch-env file (NAME=value
|
|
218
222
|
// lines), so plan.launchEnv records them and every Writer hook replays them (decision: record and replay)
|
|
@@ -243,6 +247,8 @@ async function runWriter(ctx, lane, env, { story, out, lineSlug, reviewCommitted
|
|
|
243
247
|
* keeps it (story-attempt.sh:2666–2688) and the next generation reads it (generator/ladder.sh). */
|
|
244
248
|
function persistFailures(out, laneLog) {
|
|
245
249
|
const f = path.join(out, 'checks', 'runs', 'pipeline-env', 'logs', 'story-failures.jsonl');
|
|
250
|
+
// a provider build the session itself made is stamped in the lane's log too (generator/sibling-stamps.js)
|
|
251
|
+
if (laneLog) require('../generator/sibling-stamps.js').persist(path.join(out, 'checks', 'runs', 'pipeline-env', 'logs'), laneLog);
|
|
246
252
|
if (!laneLog || !fs.existsSync(f)) return;
|
|
247
253
|
fs.mkdirSync(laneLog, { recursive: true });
|
|
248
254
|
fs.copyFileSync(f, path.join(laneLog, 'story-failures.jsonl'));
|
package/generator/generate.js
CHANGED
|
@@ -149,6 +149,9 @@ function main(a) {
|
|
|
149
149
|
fs.mkdirSync(path.join(checksState, 'pipeline-env', 'logs'), { recursive: true });
|
|
150
150
|
fs.copyFileSync(path.join(process.env.LOG_DIR, 'story-failures.jsonl'), path.join(checksState, 'pipeline-env', 'logs', 'story-failures.jsonl'));
|
|
151
151
|
}
|
|
152
|
+
// THE "PROVIDER BUILT" STAMPS (generator/sibling-stamps.js): the driver builds a sibling provider before the Writer (driver/sibling-prebuild.js) and stamps it in the lane's log; Step 19's hook inside the session finds the
|
|
153
|
+
// stamp here and does not build it again (run 11: that build, 3.2 GB, is what the session's 4096 MB scope could not hold)
|
|
154
|
+
if (process.env.LOG_DIR) require('./sibling-stamps.js').seed(process.env.LOG_DIR, path.join(checksState, 'pipeline-env', 'logs'));
|
|
152
155
|
// the plan the run works from, published as the pipeline publishes it after the spec pass (hooks/publish-agent-outputs.sh),
|
|
153
156
|
// in the hooks' state before any hook renders from it
|
|
154
157
|
const pub = spawnSync('bash', [path.join(__dirname, '..', 'hooks', 'publish-agent-outputs.sh'), pipelineJson, checksState], { encoding: 'utf8' });
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
// DOES THE DETECTIVE'S STRUCTURED ANALYSIS GIVE THIS CODELINE NO CHANGE? (run 11, 2026-10-21.) The pipeline's plan-gives-no-change.js reads prose (the consumer's `change` line must be exactly "none"); the same story carries the
|
|
4
|
+
// detective's structured findings — every fix site has `changeRequired` — and a codeline whose sites ALL say false needs no edit: its files are "NO EDIT REQUIRED", verified, not changed. Exit 0 = no change (and the reason on
|
|
5
|
+
// stdout as JSON); exit 1 = otherwise: no analysis for the lane, no sites, or ANY site that is true or does not say (no evidence is not "no change").
|
|
6
|
+
// plan-no-change-structured.js <prd> <story-id> <lane codeline>
|
|
7
|
+
const fs = require('fs');
|
|
8
|
+
|
|
9
|
+
try {
|
|
10
|
+
const [prdFile, storyId, lane] = process.argv.slice(2);
|
|
11
|
+
const prd = JSON.parse(fs.readFileSync(prdFile, 'utf8'));
|
|
12
|
+
const s = (prd.stories || []).find((x) => x && x.id === storyId);
|
|
13
|
+
if (!s) process.exit(1);
|
|
14
|
+
// ONLY A LANE THE PLAN SAYS MERELY RECEIVES THE CHANGE: a consumer in the story's overall plan and the provider of none of its packages. The provider is where the change is made — and run 9's detective marked the two
|
|
15
|
+
// sites cxshared edited changeRequired:false — so for it the field is not evidence of anything.
|
|
16
|
+
const pkgs = (s.overallPlan && Array.isArray(s.overallPlan.packages)) ? s.overallPlan.packages : [];
|
|
17
|
+
const isConsumer = pkgs.some((p) => p && (p.consumers || []).some((c) => c && c.codeline === lane));
|
|
18
|
+
const isProvider = pkgs.some((p) => p && p.providedBy === lane);
|
|
19
|
+
if (!isConsumer || isProvider) process.exit(1);
|
|
20
|
+
const per = s.fixSiteAnalysisPerCodeline && typeof s.fixSiteAnalysisPerCodeline === 'object' ? s.fixSiteAnalysisPerCodeline : {};
|
|
21
|
+
const sites = Array.isArray(per[lane]) ? per[lane] : (s.codeline === lane && Array.isArray(s.fixSiteAnalysis) ? s.fixSiteAnalysis : null);
|
|
22
|
+
if (!sites || !sites.length || !sites.every((x) => x && x.changeRequired === false)) process.exit(1);
|
|
23
|
+
process.stdout.write(`${JSON.stringify({ sites: sites.length, reason: `none of its ${sites.length} fix sites has changeRequired (the detective's structured analysis)` })}\n`);
|
|
24
|
+
process.exit(0);
|
|
25
|
+
} catch { process.exit(1); }
|
|
@@ -14,7 +14,13 @@ LANE_LOG="${LOG_DIR:-}"
|
|
|
14
14
|
set +e
|
|
15
15
|
for _np in "${PRD_FILE:-}" "${MAIN_PRD_FILE:-}"; do
|
|
16
16
|
[ -n "$_np" ] && [ -f "$_np" ] || continue
|
|
17
|
-
"
|
|
17
|
+
# THE PIPELINE'S ROUTE (the consumer's change line is exactly "none"), else THE STRUCTURED ONE (generator/plan-no-change-structured.js: every fix site of this lane's analysis says changeRequired false) — run 11:
|
|
18
|
+
# the plan's prose was "Verify … no manifest change." and a Writer was sent into 8 failing attempts over files the detective had already marked NO EDIT REQUIRED
|
|
19
|
+
_why=""
|
|
20
|
+
if ! "$NODE_BIN" "$SCRIPT_DIR/lib/handlers/plan-gives-no-change.js" "$_np" "$STORY" "$LANE" 2>/dev/null; then
|
|
21
|
+
_sj=$("$NODE_BIN" "$(dirname "${BASH_SOURCE[0]}")/plan-no-change-structured.js" "$_np" "$STORY" "$LANE" 2>/dev/null) || continue
|
|
22
|
+
_why=" ($(printf '%s' "$_sj" | jq -r '.reason // empty' 2>/dev/null))"
|
|
23
|
+
fi
|
|
18
24
|
_ref=""
|
|
19
25
|
if [ -e "$PROJECT_ROOT/.git" ]; then
|
|
20
26
|
[ -n "$LANE_LOG" ] && [ -f "$LANE_LOG/phase-baseline-sha.txt" ] && _ref=$(tr -d '[:space:]' < "$LANE_LOG/phase-baseline-sha.txt" 2>/dev/null)
|
|
@@ -26,7 +32,7 @@ for _np in "${PRD_FILE:-}" "${MAIN_PRD_FILE:-}"; do
|
|
|
26
32
|
'{noChange:false, message:$m}'
|
|
27
33
|
exit 0
|
|
28
34
|
fi
|
|
29
|
-
jq -cn --arg m "Story $STORY: the story's plan gives $LANE no change — no writer is run; the lane's gates prove it against the release it receives" \
|
|
35
|
+
jq -cn --arg m "Story $STORY: the story's plan gives $LANE no change${_why} — no writer is run; the lane's gates prove it against the release it receives" \
|
|
30
36
|
'{noChange:true, message:$m}'
|
|
31
37
|
exit 0
|
|
32
38
|
done
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
// THE "BUILT ONCE" STAMPS OF SIBLING PROVIDERS (lib/codeline-lanes.sh link_sibling_builds: `sibling-build-<hash of the provider's path>-<its HEAD>` in LOG_DIR, written when the provider's build succeeded). The Bash pipeline's LOG_DIR
|
|
3
|
+
// is the lane's persistent log; the Writer's hooks run with a per-invocation state folder, so a stamp made in the lane's log (by driver/sibling-prebuild.js, outside the session) is COPIED into the state before the session
|
|
4
|
+
// (seed), and one a session made is copied back (persist). Only stamp files move — never the build logs or anything else. No AI.
|
|
5
|
+
const fs = require('fs');
|
|
6
|
+
const path = require('path');
|
|
7
|
+
|
|
8
|
+
const STAMP = /^sibling-build-[0-9a-f]{12}-[0-9a-z]+$/;
|
|
9
|
+
|
|
10
|
+
function copyStamps(from, to) {
|
|
11
|
+
let names = [];
|
|
12
|
+
try { names = fs.readdirSync(from).filter((n) => STAMP.test(n)); } catch { return []; }
|
|
13
|
+
const moved = [];
|
|
14
|
+
for (const n of names) {
|
|
15
|
+
if (fs.existsSync(path.join(to, n))) continue;
|
|
16
|
+
fs.mkdirSync(to, { recursive: true }); fs.copyFileSync(path.join(from, n), path.join(to, n)); moved.push(n);
|
|
17
|
+
}
|
|
18
|
+
return moved;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** The lane's stamps into the Writer's state logs; returns the names copied. */
|
|
22
|
+
const seed = (laneLog, stateLogs) => copyStamps(laneLog, stateLogs);
|
|
23
|
+
/** The stamps a session made, back into the lane's log; returns the names copied. */
|
|
24
|
+
const persist = (stateLogs, laneLog) => copyStamps(stateLogs, laneLog);
|
|
25
|
+
|
|
26
|
+
module.exports = { seed, persist, STAMP };
|
package/package.json
CHANGED