amsd-pipeline-workflow 0.1.12 → 0.1.14
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/driver/release-lines.js +18 -1
- package/driver/run.js +43 -2
- package/driver/stages.js +3 -2
- package/generator/generate.js +3 -0
- package/generator/writer-workflow.js +8 -0
- package/package.json +1 -1
package/driver/release-lines.js
CHANGED
|
@@ -43,6 +43,11 @@ async function runReleaseLines(ctx, lane, clPrd, laneLog, laneEnv, canonical, ph
|
|
|
43
43
|
const lineLog = path.join(laneLog, 'lines', slug); fs.mkdirSync(lineLog, { recursive: true });
|
|
44
44
|
const linePrd = path.join(lineLog, 'prd.json');
|
|
45
45
|
let rerun = ['--reset'];
|
|
46
|
+
// A RESUMED LINE'S COMMITTED WORK IS NOT REWRITTEN (operator, 2026-10-20): its story completed by its commit, as a lane's is
|
|
47
|
+
if (env.EPAM_RESUME_RUN && fs.existsSync(linePrd)) {
|
|
48
|
+
const id = (JSON.parse(fs.readFileSync(linePrd, 'utf8')).stories[0] || {}).id;
|
|
49
|
+
if (id) completeLineByCommit(env, wt, lineLog, linePrd, `${env.EPAM_BRANCH_PREFIX || ''}AI-${id}-${slug}`, log);
|
|
50
|
+
}
|
|
46
51
|
const done = fs.existsSync(linePrd) && (() => { const p = JSON.parse(fs.readFileSync(linePrd, 'utf8')); return p.stories.length > 0 && p.stories.every((s) => s.status === 'completed'); })();
|
|
47
52
|
if (bash(env, 'resume_preserves', ['completed-stories']).status === 0 && done
|
|
48
53
|
&& !handler(env, 'spec-predates-plan', [linePrd, canonical || clPrd]).stdout.trim()) { // :96–106
|
|
@@ -89,4 +94,16 @@ async function runReleaseLines(ctx, lane, clPrd, laneLog, laneEnv, canonical, ph
|
|
|
89
94
|
}
|
|
90
95
|
}
|
|
91
96
|
|
|
92
|
-
|
|
97
|
+
/**
|
|
98
|
+
* A RESUMED RELEASE LINE (LIVE-AMSD-2974-11): the line's branch checked out and the pipeline's own
|
|
99
|
+
* story_resume_completed_by_commit run on the line's log and PRD. The pipeline applies it to lanes only, so a line whose
|
|
100
|
+
* work was committed before its session died was reset and rewritten. A story committed on the line branch since the
|
|
101
|
+
* line's baseline is completed; the line's "completed" path then has its reviewer judge it (locked once approved).
|
|
102
|
+
*/
|
|
103
|
+
function completeLineByCommit(env, wt, lineLog, linePrd, branch, log) {
|
|
104
|
+
if (git(wt, 'rev-parse', '--verify', '--quiet', branch).status !== 0) return;
|
|
105
|
+
if (git(wt, 'checkout', '-q', branch).status !== 0) { log(`[orch] could not check out ${branch} to read its committed work`); return; }
|
|
106
|
+
bash({ ...env, PROJECT_ROOT: wt }, 'story_resume_completed_by_commit', [wt, lineLog, linePrd], { log });
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
module.exports = { runReleaseLines, completeLineByCommit };
|
package/driver/run.js
CHANGED
|
@@ -404,6 +404,25 @@ function provisionCodeline(ctx, cl, wt) {
|
|
|
404
404
|
if (fs.existsSync(ap)) { fs.mkdirSync(epam, { recursive: true }); fs.copyFileSync(ap, path.join(epam, 'anti-patterns.json')); }
|
|
405
405
|
}
|
|
406
406
|
|
|
407
|
+
/** A story's newest review record: the lane log's review-verdict-<story>.json, else (a run from before that record) the
|
|
408
|
+
* newest review-feedback.json any of this lane's Writers left; null when there is none. */
|
|
409
|
+
function reviewRecord(ctx, lane, env, id, lineSlug) {
|
|
410
|
+
const read = (f) => { try { return JSON.parse(fs.readFileSync(f, 'utf8')); } catch { return null; } };
|
|
411
|
+
const v = env.LOG_DIR && read(path.join(env.LOG_DIR, `review-verdict-${id}.json`));
|
|
412
|
+
if (v) return { verdict: v.verdict, from: `review-verdict-${id}.json` };
|
|
413
|
+
// THIS LINE'S Writers only: a release line is never locked by another line's approval
|
|
414
|
+
const base = path.join(ctx.runDir, 'workflows', lane.cl, lineSlug ? `line-${lineSlug}` : 'main');
|
|
415
|
+
const found = [];
|
|
416
|
+
for (const tag of (fs.existsSync(base) ? fs.readdirSync(base) : [])) {
|
|
417
|
+
const f = path.join(base, tag, 'writer', id, 'checks', 'runs', 'review-feedback.json');
|
|
418
|
+
if (fs.existsSync(f)) found.push({ f, t: fs.statSync(f).mtimeMs });
|
|
419
|
+
}
|
|
420
|
+
if (!found.length) return null;
|
|
421
|
+
const newest = found.sort((a, b) => b.t - a.t)[0];
|
|
422
|
+
const fb = read(newest.f);
|
|
423
|
+
return fb ? { verdict: fb.verdict, from: path.relative(ctx.runDir, newest.f) } : null;
|
|
424
|
+
}
|
|
425
|
+
|
|
407
426
|
// ── ONE PHASE OF ONE LANE: what `bash run-agent-orchestration.sh` does for it, around the two workflows ─────────────
|
|
408
427
|
// Returns the phase's exit code (lib/phase-exit.sh).
|
|
409
428
|
async function phaseRunner(ctx, lane, laneEnv, { prd, phase, args, tag, lineSlug }) {
|
|
@@ -425,6 +444,14 @@ async function phaseRunner(ctx, lane, laneEnv, { prd, phase, args, tag, lineSlug
|
|
|
425
444
|
const p = JSON.parse(fs.readFileSync(prd, 'utf8'));
|
|
426
445
|
const rq = (s) => ((s.status === 'failed' || s.status === 'blocked') && !(s.completed) ? { ...s, status: 'pending' } : s);
|
|
427
446
|
p.stories = (p.stories || []).map(rq);
|
|
447
|
+
// ONCE REVIEWED, LOCKED (operator, 2026-10-20): a completed story whose newest review record is an approval is neither
|
|
448
|
+
// rewritten nor re-reviewed; an "escalated" stamp an EARLIER attempt left is cleared as the pipeline clears it on
|
|
449
|
+
// approval (lib/review-cycle.sh ~:180–195) — the stale stamp would otherwise fail Step 5 on work already approved
|
|
450
|
+
for (const s of p.stories) {
|
|
451
|
+
if (!(s.completed === true && s.reviewStatus === 'escalated')) continue;
|
|
452
|
+
const rec = reviewRecord(ctx, lane, env, s.id, lineSlug);
|
|
453
|
+
if (rec && rec.verdict === 'approved') { s.reviewStatus = null; log(`[driver] ${s.id}: locked — reviewed and approved (${rec.from}); its earlier escalated stamp is cleared, nothing is rewritten or re-reviewed`); }
|
|
454
|
+
}
|
|
428
455
|
fs.writeFileSync(prd, JSON.stringify(p, null, 2));
|
|
429
456
|
}
|
|
430
457
|
// RAO:1112–1123 — the spec pass, unless the lane resumed with its spec done
|
|
@@ -438,6 +465,12 @@ async function phaseRunner(ctx, lane, laneEnv, { prd, phase, args, tag, lineSlug
|
|
|
438
465
|
const p = JSON.parse(fs.readFileSync(prd, 'utf8'));
|
|
439
466
|
const order = ((p.implementationOrder || {})[phase] || []).map((id) => p.stories.find((s) => s.id === id))
|
|
440
467
|
.filter((s) => s && s.status !== 'deprecated' && !s.completed && s.agentRole !== 'review-agent');
|
|
468
|
+
// COMMITTED BUT NEVER REVIEWED (a resume: the pipeline completed it by its commit): reviewed, not rewritten — once
|
|
469
|
+
// reviewed, locked (operator, 2026-10-20); an approved one is locked and not touched
|
|
470
|
+
const toReview = env.EPAM_RESUME_RUN ? ((p.implementationOrder || {})[phase] || []).map((id) => p.stories.find((s) => s.id === id))
|
|
471
|
+
.filter((s) => s && s.completed === true && s.status !== 'deprecated' && s.agentRole !== 'review-agent')
|
|
472
|
+
.filter((s) => { const rec = reviewRecord(ctx, lane, env, s.id, lineSlug); return !(rec && rec.verdict === 'approved'); }) : [];
|
|
473
|
+
for (const s of toReview) log(`[driver] ${s.id}: committed but never reviewed — its review judges the committed change; nothing is rewritten`);
|
|
441
474
|
if (order.length && env.EPAM_BROWNFIELD === '1') bash(env, 'ensure_story_branch', [lane.wt, order[0].id, env.JIRA_BASELINE_BRANCH || ''], { log });
|
|
442
475
|
// RAO:2653–2654 — THE LANE'S PHASE BASELINE (lib/qa-gate-evidence.sh qa_record_phase_baseline: the fork point from the
|
|
443
476
|
// base branch) in the lane's log, where the release lines read the main line's change from (lib/release-lines.sh:69–74)
|
|
@@ -445,11 +478,12 @@ async function phaseRunner(ctx, lane, laneEnv, { prd, phase, args, tag, lineSlug
|
|
|
445
478
|
// RAO:3094–3323 — Steps 11 and 12, once per phase before its stories (driver/phase-audits.js)
|
|
446
479
|
if (order.length) runPhaseAudits(env, { wt: lane.wt, phase }, log);
|
|
447
480
|
// the Writer, per story in the phase's order (docs/DRIVER.md: Steps 3.54–3.6, 19 and 20 run once per story)
|
|
448
|
-
for (const s of order) {
|
|
481
|
+
for (const s of [...order, ...toReview]) {
|
|
482
|
+
const reviewCommitted = toReview.includes(s);
|
|
449
483
|
// lib/cli-commands.sh:501–504 — the per-story guards: cost budget, pause, redirect (driver/story-guards.js)
|
|
450
484
|
storyGuards(env, { prd, story: s.id, checkpointPrd: path.join(ctx.env.EPAM_PROJECT_CONFIG_DIR, 'runs', ctx.run, 'lanes', lane.cl,
|
|
451
485
|
...(lineSlug ? ['lines', lineSlug] : []), 'checkpoint', 'prd.json') }, log);
|
|
452
|
-
const res = await stages.runWriter(ctx, lane, env, { story: s.id, out: path.join(dir, 'writer', s.id), lineSlug });
|
|
486
|
+
const res = await stages.runWriter(ctx, lane, env, { story: s.id, out: path.join(dir, 'writer', s.id), lineSlug, reviewCommitted });
|
|
453
487
|
log(`[driver] ${lane.cl} ${s.id}: writer → ${res.status} (exit ${res.exit})${res.reason ? ` — ${res.reason}` : ''}`);
|
|
454
488
|
if (blockCap.signature(s.id, res)) ctx.lastBlock = blockCap.signature(s.id, res);
|
|
455
489
|
// story-attempt.sh:2561–2578, :2865 — the ladder position the next invocation resumes at, in the lane's log, by the
|
|
@@ -471,6 +505,13 @@ async function phaseRunner(ctx, lane, laneEnv, { prd, phase, args, tag, lineSlug
|
|
|
471
505
|
}
|
|
472
506
|
// what Steps 3.545 and 3.55 stamp on the story, so a retry inherits the finding (driver/writer-stamps.js)
|
|
473
507
|
if (st) applyGateStamps(st, res);
|
|
508
|
+
// THE LOCK, RECORDED where a later Writer cannot wipe it (the lane's log; its out dir is rebuilt each leg, B64): a Writer
|
|
509
|
+
// that ends 'done' ended on an approved review
|
|
510
|
+
if (res.status === 'done') {
|
|
511
|
+
const head = spawnSync('git', ['-C', lane.wt, 'rev-parse', 'HEAD'], { encoding: 'utf8' });
|
|
512
|
+
fs.mkdirSync(env.LOG_DIR, { recursive: true });
|
|
513
|
+
fs.writeFileSync(path.join(env.LOG_DIR, `review-verdict-${s.id}.json`), `${JSON.stringify({ verdict: 'approved', head: head.status === 0 ? head.stdout.trim() : null, at: new Date().toISOString() })}\n`);
|
|
514
|
+
}
|
|
474
515
|
fs.writeFileSync(prd, JSON.stringify(cur, null, 2));
|
|
475
516
|
// lib/prd-state.sh update_story_status (the pipeline's), on the lane's PRD
|
|
476
517
|
bash({ ...env, MAIN_PRD_FILE: prd, PRD_FILE: prd }, 'update_story_status', [s.id, res.exit === 0 ? 'completed' : 'failed'], { log });
|
package/driver/stages.js
CHANGED
|
@@ -207,7 +207,7 @@ function overlay(ctx, lane, lineSlug) {
|
|
|
207
207
|
return dest;
|
|
208
208
|
}
|
|
209
209
|
|
|
210
|
-
async function runWriter(ctx, lane, env, { story, out, lineSlug }) {
|
|
210
|
+
async function runWriter(ctx, lane, env, { story, out, lineSlug, reviewCommitted }) {
|
|
211
211
|
fs.rmSync(out, { recursive: true, force: true });
|
|
212
212
|
const inst = overlay(ctx, lane, lineSlug);
|
|
213
213
|
// the run's recorded launch settings (driver/launch-env.js) go to the generator as its --launch-env file (NAME=value
|
|
@@ -216,7 +216,8 @@ async function runWriter(ctx, lane, env, { story, out, lineSlug }) {
|
|
|
216
216
|
fs.mkdirSync(path.dirname(lf), { recursive: true });
|
|
217
217
|
fs.writeFileSync(lf, Object.entries(ctx.launchEnv || {}).map(([k, v]) => `${k}=${v}`).join('\n') + '\n');
|
|
218
218
|
gen('generate.js', ['--install', inst, '--project', ctx.project, '--run', ctx.run, '--lane', lane.cl, '--story', story,
|
|
219
|
-
'--codeline', lane.wt, '--out', out, '--set', ctx.set, ...(Object.keys(ctx.launchEnv || {}).length ? ['--launch-env', lf] : [])
|
|
219
|
+
'--codeline', lane.wt, '--out', out, '--set', ctx.set, ...(Object.keys(ctx.launchEnv || {}).length ? ['--launch-env', lf] : []),
|
|
220
|
+
...(reviewCommitted ? ['--review-committed', 'yes'] : [])], env);
|
|
220
221
|
const plan = JSON.parse(fs.readFileSync(path.join(out, 'plan.json'), 'utf8'));
|
|
221
222
|
// THE RUN'S LAUNCH SETTINGS, AS THE GENERATOR RECORDED THEM (plan.launchEnv, the Writer port's field): they must be the
|
|
222
223
|
// settings this run was launched with — read, never re-derived here
|
package/generator/generate.js
CHANGED
|
@@ -481,6 +481,9 @@ function main(a) {
|
|
|
481
481
|
roleRefused,
|
|
482
482
|
noChange,
|
|
483
483
|
planNote,
|
|
484
|
+
// COMMITTED BUT NOT REVIEWED (a resumed story the pipeline completed by its commit with no approved review): the Writer
|
|
485
|
+
// reviews the change as it is, no writer attempt (once reviewed, locked — operator, 2026-10-20)
|
|
486
|
+
reviewCommitted: a['review-committed'] === 'yes' ? { message: `Story ${a.story}: its work is committed in this run and was never reviewed — the review judges the committed change; nothing is rewritten` } : null,
|
|
484
487
|
resume: (resume.attempt > 0 || resume.model) ? resume : null,
|
|
485
488
|
story: { id: story.id, title: story.title || '', description: story.description || '', acs },
|
|
486
489
|
// Run state at pause 2; the workflow carries the current value forward as hooks report changes (D5).
|
|
@@ -646,6 +646,14 @@ if (PLAN.noChange) {
|
|
|
646
646
|
}
|
|
647
647
|
reReview = checks
|
|
648
648
|
}
|
|
649
|
+
// COMMITTED BUT NOT REVIEWED (PLAN.reviewCommitted; story_resume_completed_by_commit's "the review judges the committed
|
|
650
|
+
// change"): the review inputs from the codeline as it is, and the loop's first pass reviews them — no writer attempt; the
|
|
651
|
+
// committed reproducing test stands (Step 3.54 is not run again). Approved, the story is done and locked.
|
|
652
|
+
if (PLAN.reviewCommitted && !reReview) {
|
|
653
|
+
log(PLAN.reviewCommitted.message)
|
|
654
|
+
reproDone = true
|
|
655
|
+
reReview = await refreshed({ passed: true, problems: [], changes: '' }, 'review inputs: the committed change')
|
|
656
|
+
}
|
|
649
657
|
// PLAN MODE AND THE PLANNING PHASE (feature plan-mode; story-attempt.sh:980–984, :1103, :1207–1216, hooks/plan-round.sh):
|
|
650
658
|
// the pipeline's own functions decide whether either runs and build every question; the planning agents answer one at a
|
|
651
659
|
// time; a reviewed plan wraps every attempt's prompt (:1617–1625). A call whose agent ended without an answer fails, as
|
package/package.json
CHANGED