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.
@@ -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
- module.exports = { runReleaseLines };
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] : [])], env);
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
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "amsd-pipeline-workflow",
3
- "version": "0.1.12",
3
+ "version": "0.1.14",
4
4
  "description": "The AMSD pipeline as Claude workflows. Installed by the amsd-pipeline installer: install.sh --workflows",
5
5
  "license": "UNLICENSED",
6
6
  "bin": {