amsd-pipeline-workflow 0.1.13 → 0.1.15
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 +33 -2
- package/driver/run.js +15 -7
- 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,7 +43,12 @@ 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
|
-
|
|
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
|
+
}
|
|
51
|
+
const done = lineIsDone(linePrd);
|
|
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
|
|
49
54
|
rerun = [];
|
|
@@ -89,4 +94,30 @@ 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
|
+
// THE PIPELINE'S FUNCTION SETS completed ONLY; the line's done test reads status (lib/release-lines.sh:96), so a line
|
|
108
|
+
// completed by its commit read as not done, took the --reset path and was rewritten (LIVE-AMSD-2974-11, 2026-10-20).
|
|
109
|
+
// A Writer-completed story carries both; this one does too.
|
|
110
|
+
const p = JSON.parse(fs.readFileSync(linePrd, 'utf8'));
|
|
111
|
+
let changed = false;
|
|
112
|
+
for (const s of p.stories || []) if (s.completed === true && s.status !== 'completed' && s.status !== 'deprecated') { s.status = 'completed'; changed = true; }
|
|
113
|
+
if (changed) fs.writeFileSync(linePrd, JSON.stringify(p, null, 2));
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** A line whose every story is completed — the pipeline's own test (lib/release-lines.sh:96). */
|
|
117
|
+
function lineIsDone(linePrd) {
|
|
118
|
+
if (!fs.existsSync(linePrd)) return false;
|
|
119
|
+
const p = JSON.parse(fs.readFileSync(linePrd, 'utf8'));
|
|
120
|
+
return (p.stories || []).length > 0 && p.stories.every((s) => s.status === 'completed');
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
module.exports = { runReleaseLines, completeLineByCommit, lineIsDone };
|
package/driver/run.js
CHANGED
|
@@ -406,14 +406,15 @@ function provisionCodeline(ctx, cl, wt) {
|
|
|
406
406
|
|
|
407
407
|
/** A story's newest review record: the lane log's review-verdict-<story>.json, else (a run from before that record) the
|
|
408
408
|
* newest review-feedback.json any of this lane's Writers left; null when there is none. */
|
|
409
|
-
function reviewRecord(ctx, lane, env, id) {
|
|
409
|
+
function reviewRecord(ctx, lane, env, id, lineSlug) {
|
|
410
410
|
const read = (f) => { try { return JSON.parse(fs.readFileSync(f, 'utf8')); } catch { return null; } };
|
|
411
411
|
const v = env.LOG_DIR && read(path.join(env.LOG_DIR, `review-verdict-${id}.json`));
|
|
412
412
|
if (v) return { verdict: v.verdict, from: `review-verdict-${id}.json` };
|
|
413
|
-
|
|
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');
|
|
414
415
|
const found = [];
|
|
415
|
-
for (const
|
|
416
|
-
const f = path.join(base,
|
|
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');
|
|
417
418
|
if (fs.existsSync(f)) found.push({ f, t: fs.statSync(f).mtimeMs });
|
|
418
419
|
}
|
|
419
420
|
if (!found.length) return null;
|
|
@@ -448,7 +449,7 @@ async function phaseRunner(ctx, lane, laneEnv, { prd, phase, args, tag, lineSlug
|
|
|
448
449
|
// approval (lib/review-cycle.sh ~:180–195) — the stale stamp would otherwise fail Step 5 on work already approved
|
|
449
450
|
for (const s of p.stories) {
|
|
450
451
|
if (!(s.completed === true && s.reviewStatus === 'escalated')) continue;
|
|
451
|
-
const rec = reviewRecord(ctx, lane, env, s.id);
|
|
452
|
+
const rec = reviewRecord(ctx, lane, env, s.id, lineSlug);
|
|
452
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`); }
|
|
453
454
|
}
|
|
454
455
|
fs.writeFileSync(prd, JSON.stringify(p, null, 2));
|
|
@@ -464,6 +465,12 @@ async function phaseRunner(ctx, lane, laneEnv, { prd, phase, args, tag, lineSlug
|
|
|
464
465
|
const p = JSON.parse(fs.readFileSync(prd, 'utf8'));
|
|
465
466
|
const order = ((p.implementationOrder || {})[phase] || []).map((id) => p.stories.find((s) => s.id === id))
|
|
466
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`);
|
|
467
474
|
if (order.length && env.EPAM_BROWNFIELD === '1') bash(env, 'ensure_story_branch', [lane.wt, order[0].id, env.JIRA_BASELINE_BRANCH || ''], { log });
|
|
468
475
|
// RAO:2653–2654 — THE LANE'S PHASE BASELINE (lib/qa-gate-evidence.sh qa_record_phase_baseline: the fork point from the
|
|
469
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)
|
|
@@ -471,11 +478,12 @@ async function phaseRunner(ctx, lane, laneEnv, { prd, phase, args, tag, lineSlug
|
|
|
471
478
|
// RAO:3094–3323 — Steps 11 and 12, once per phase before its stories (driver/phase-audits.js)
|
|
472
479
|
if (order.length) runPhaseAudits(env, { wt: lane.wt, phase }, log);
|
|
473
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)
|
|
474
|
-
for (const s of order) {
|
|
481
|
+
for (const s of [...order, ...toReview]) {
|
|
482
|
+
const reviewCommitted = toReview.includes(s);
|
|
475
483
|
// lib/cli-commands.sh:501–504 — the per-story guards: cost budget, pause, redirect (driver/story-guards.js)
|
|
476
484
|
storyGuards(env, { prd, story: s.id, checkpointPrd: path.join(ctx.env.EPAM_PROJECT_CONFIG_DIR, 'runs', ctx.run, 'lanes', lane.cl,
|
|
477
485
|
...(lineSlug ? ['lines', lineSlug] : []), 'checkpoint', 'prd.json') }, log);
|
|
478
|
-
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 });
|
|
479
487
|
log(`[driver] ${lane.cl} ${s.id}: writer → ${res.status} (exit ${res.exit})${res.reason ? ` — ${res.reason}` : ''}`);
|
|
480
488
|
if (blockCap.signature(s.id, res)) ctx.lastBlock = blockCap.signature(s.id, res);
|
|
481
489
|
// story-attempt.sh:2561–2578, :2865 — the ladder position the next invocation resumes at, in the lane's log, by the
|
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