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.
@@ -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
- 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'); })();
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
- 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
+ // 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
- const base = path.join(ctx.runDir, 'workflows', lane.cl);
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 line of (fs.existsSync(base) ? fs.readdirSync(base) : [])) for (const tag of (fs.existsSync(path.join(base, line)) ? fs.readdirSync(path.join(base, line)) : [])) {
416
- const f = path.join(base, line, tag, 'writer', id, 'checks', 'runs', 'review-feedback.json');
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] : [])], 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.13",
3
+ "version": "0.1.15",
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": {