amsd-pipeline-workflow 0.1.19 → 0.1.21
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/bin/cpu-bound +22 -0
- package/doctor/profile.json +15 -4
- package/driver/block-cap.js +2 -1
- package/driver/runtime.js +2 -1
- package/generator/agents-workflow.js +3 -1
- package/generator/pack-plan.js +74 -0
- package/generator/spec-workflow.js +3 -1
- package/generator/writer-workflow.js +3 -1
- package/package.json +1 -1
package/bin/cpu-bound
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# cpu-bound <command...> — the live session's tools may not size their pools from the whole machine.
|
|
3
|
+
#
|
|
4
|
+
# Run 11, 2026-10-20 23:07 UTC: the Writer agent ran the codeline's whole jest suite; jest sized 15 workers from the host's 16 cores
|
|
5
|
+
# (~270 MB each) inside the session's 4096 MB memory scope, and the kernel OOM-killed the session. The Bash pipeline fixed exactly this in
|
|
6
|
+
# run 9 (lib/bounded-exec.sh): the number of workers is DERIVED from the tightest cgroup memory limit (resolve_test_workers), and the
|
|
7
|
+
# process's CPU affinity is restricted to that many CPUs (bound_self_affinity) — everything it starts, the agent's own tool commands
|
|
8
|
+
# included, inherits it and sizes its pool from it; a preload (lib/node-cpu-bound.js) makes runners that count os.cpus() see the bound too.
|
|
9
|
+
# Those are the PIPELINE's functions, sourced from the install (SCRIPT_DIR); nothing is copied here.
|
|
10
|
+
#
|
|
11
|
+
# Placed INSIDE the memory scope (bin/bounded), so the cgroup it reads is the one the session lives in: bounded cpu-bound claude -p …
|
|
12
|
+
# A bound that cannot be applied is SAID (stderr) and the command still runs — as the pipeline's own wrapper does.
|
|
13
|
+
set -u
|
|
14
|
+
[ $# -gt 0 ] || { echo "cpu-bound: no command" >&2; exit 64; }
|
|
15
|
+
if [ -n "${SCRIPT_DIR:-}" ] && [ -f "$SCRIPT_DIR/lib/bounded-exec.sh" ]; then
|
|
16
|
+
# shellcheck source=/dev/null
|
|
17
|
+
. "$SCRIPT_DIR/lib/bounded-exec.sh"
|
|
18
|
+
bound_self_affinity "$(resolve_test_workers)"
|
|
19
|
+
else
|
|
20
|
+
echo "[cpu-bound] WARNING: the pipeline's lib/bounded-exec.sh was not found (SCRIPT_DIR='${SCRIPT_DIR:-}') — this session's tools run UNBOUNDED" >&2
|
|
21
|
+
fi
|
|
22
|
+
exec "$@"
|
package/doctor/profile.json
CHANGED
|
@@ -61,12 +61,22 @@
|
|
|
61
61
|
"never works a real codeline",
|
|
62
62
|
"cannot resume run",
|
|
63
63
|
"operator's yes",
|
|
64
|
-
"needs the operator"
|
|
64
|
+
"needs the operator",
|
|
65
|
+
"Missing required env vars",
|
|
66
|
+
"ingest reads Jira",
|
|
67
|
+
"UNHEALTHY",
|
|
68
|
+
"greenfield scaffolding is off",
|
|
69
|
+
"Brownfield worktree is not a git repository",
|
|
70
|
+
"the manifest could not be read",
|
|
71
|
+
", not a copy"
|
|
65
72
|
],
|
|
66
73
|
"faultMarks": [
|
|
67
74
|
"\\(exit undefined\\)",
|
|
68
|
-
"\\(exit NaN\\)"
|
|
69
|
-
|
|
75
|
+
"\\(exit NaN\\)",
|
|
76
|
+
"the same block \\d+ times in a row",
|
|
77
|
+
"the self-healing retry changed nothing"
|
|
78
|
+
],
|
|
79
|
+
"policy": "all-but-refusals"
|
|
70
80
|
},
|
|
71
81
|
"settings": {
|
|
72
82
|
"on": "EPAM_PIPELINE_DOCTOR",
|
|
@@ -89,7 +99,8 @@
|
|
|
89
99
|
"evidence": [
|
|
90
100
|
"driver.log"
|
|
91
101
|
],
|
|
92
|
-
"evidenceTailLines": 200
|
|
102
|
+
"evidenceTailLines": 200,
|
|
103
|
+
"legStart": "^\\[launch\\] replayed"
|
|
93
104
|
},
|
|
94
105
|
"branch": {
|
|
95
106
|
"prefix": "doctor/"
|
package/driver/block-cap.js
CHANGED
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
// retries"). A recorded difference from the pipeline (backlog B39): its lane loop retries an exit-2 phase while HEAD moves,
|
|
4
4
|
// and every Writer run moves it, so a story a gate keeps blocking retries forever. Here a phase stops — failed, naming
|
|
5
5
|
// the gate — when its last BLOCK_CAP runs ended in the same block of the same story.
|
|
6
|
-
|
|
6
|
+
// RELAXED 3 → 5 (operator, 2026-10-20: "relax limits like self heal"): the pipeline's own retry gets more runs before the phase gives up, and giving up is a fault the doctor now takes (doctor/profile.json faultMarks).
|
|
7
|
+
const BLOCK_CAP = 5;
|
|
7
8
|
|
|
8
9
|
/** What blocked a Writer run: the story and the gate (the reason up to its detail), or null when nothing blocked. */
|
|
9
10
|
function signature(story, res) {
|
package/driver/runtime.js
CHANGED
|
@@ -71,7 +71,8 @@ function liveCommand({ name, script, agentsJson, args, sessionDir, env, cwd }) {
|
|
|
71
71
|
// the session itself waits for its workflow (CLAUDE_CODE_PRINT_BG_WAIT_CEILING_MS=0 below); the runner has no task-wait tool
|
|
72
72
|
const prompt = `Run the workflow "${name}" with these args, exactly: ${JSON.stringify(args || {})}. When it has finished, reply with its result as JSON and nothing else.`;
|
|
73
73
|
const claude = (env && env.CLAUDE_BIN) || 'claude';
|
|
74
|
-
|
|
74
|
+
// memory scope (bin/bounded) → CPU bound derived from that scope (bin/cpu-bound, the pipeline's own bound_self_affinity) → the session: its tools (jest included) cannot out-size the scope (run 11 OOM)
|
|
75
|
+
const argv = [path.join(__dirname, '..', 'bin', 'bounded'), path.join(__dirname, '..', 'bin', 'cpu-bound'), claude, '-p', prompt, '--output-format', 'json',
|
|
75
76
|
'--setting-sources', 'project', '--permission-mode', 'bypassPermissions'];
|
|
76
77
|
return { cwd: home, argv, agents: Object.keys(defs).length, workflow: path.join(wfDir, `${name}.js`) };
|
|
77
78
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
const gateway = require('./gateway.js');
|
|
3
|
+
const { packPlan } = require('./pack-plan.js');
|
|
3
4
|
// THE CODELINE, AGENTS WORKFLOW: plan (data) + emitted script (decisions 10, 15, 16, 25).
|
|
4
5
|
//
|
|
5
6
|
// The script holds the stage's ORDER — the pipeline's own: the launcher's reset (orchestrate.sh), the resume
|
|
@@ -16,6 +17,7 @@ const REPLY_SCHEMA = { type: 'object', required: ['reply'], properties: { reply:
|
|
|
16
17
|
const RUN_SCHEMA = { type: 'object', properties: { done: { type: 'boolean' } } };
|
|
17
18
|
|
|
18
19
|
function emit(plan) {
|
|
20
|
+
const packed = packPlan(plan);
|
|
19
21
|
return `export const meta = {
|
|
20
22
|
name: 'codeline-agents',
|
|
21
23
|
description: 'Codeline, Agents stage: reset, resume, ingest (discovery, AC gate, PRD), agents (survey, mint, roster, roles, prompts), pause 1',
|
|
@@ -35,7 +37,7 @@ async function agentOrNull(prompt, opts) {
|
|
|
35
37
|
}
|
|
36
38
|
|
|
37
39
|
// GENERATED by amsd-pipeline-workflow/generator — do not edit. Source: ${plan.source}
|
|
38
|
-
const PLAN = ${
|
|
40
|
+
${packed.decls}const PLAN = ${packed.expr}
|
|
39
41
|
const REPLY = ${JSON.stringify(REPLY_SCHEMA)}
|
|
40
42
|
const RUN = ${JSON.stringify(RUN_SCHEMA)}
|
|
41
43
|
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
// THE PLAN, AS SOURCE FOR THE EMITTED WORKFLOW (decision 12 continued): a workflow script embeds its `plan` as a literal
|
|
3
|
+
// — the real Workflow tool's sandbox gives a script no `fs`, nothing else can reach generation-time data (test/layer1/
|
|
4
|
+
// harness.js's vm context: no `fs`, no `require`) — but `plan.prompts` can hold several near-duplicate renders (the
|
|
5
|
+
// Writer's first/retry/review variants, each the pipeline's FULL prompt with one small insertion). LIVE-AMSD-2974-11
|
|
6
|
+
// died on "Workflow \"writer\" not found. Available: deep-research, spec": three ~105KB near-copies pushed the script
|
|
7
|
+
// past the real Workflow tool's own script-size cap (524288 bytes — this repo's own Workflow tool documents the same
|
|
8
|
+
// figure). Every `prompts` value that shares a long prefix and suffix with an earlier one is written as a slice of it,
|
|
9
|
+
// not copied again; the runtime value is unchanged (packPlan's output round-trips to a deep-equal `plan`).
|
|
10
|
+
const MIN_SHARE = 200; // below this, the slice expression costs more source than copying the text saves
|
|
11
|
+
|
|
12
|
+
function sharedPrefixLen(a, b) {
|
|
13
|
+
const n = Math.min(a.length, b.length);
|
|
14
|
+
let i = 0;
|
|
15
|
+
while (i < n && a[i] === b[i]) i += 1;
|
|
16
|
+
return i;
|
|
17
|
+
}
|
|
18
|
+
function sharedSuffixLen(a, b, max) {
|
|
19
|
+
let i = 0;
|
|
20
|
+
while (i < max && a[a.length - 1 - i] === b[b.length - 1 - i]) i += 1;
|
|
21
|
+
return i;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** `prompts` (string values), as a JS object-literal expression: a value sharing >= MIN_SHARE chars of prefix+suffix
|
|
25
|
+
* with an earlier (unfactored) one is sliced from it; every other value is its own literal. */
|
|
26
|
+
function packPrompts(prompts) {
|
|
27
|
+
const keys = Object.keys(prompts);
|
|
28
|
+
const bases = [];
|
|
29
|
+
let decls = '';
|
|
30
|
+
const entries = keys.map((k) => {
|
|
31
|
+
const text = prompts[k];
|
|
32
|
+
let best = null;
|
|
33
|
+
if (typeof text === 'string') {
|
|
34
|
+
for (const b of bases) {
|
|
35
|
+
const p = sharedPrefixLen(text, b.text);
|
|
36
|
+
const s = sharedSuffixLen(text, b.text, Math.min(text.length, b.text.length) - p);
|
|
37
|
+
const saved = p + s;
|
|
38
|
+
if (saved >= MIN_SHARE && (!best || saved > best.saved)) best = { b, p, s, saved };
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
let expr;
|
|
42
|
+
if (best) {
|
|
43
|
+
const { b, p, s } = best;
|
|
44
|
+
const mid = text.slice(p, text.length - s);
|
|
45
|
+
const parts = [];
|
|
46
|
+
if (p > 0) parts.push(`${b.name}.slice(0,${p})`);
|
|
47
|
+
if (mid) parts.push(JSON.stringify(mid));
|
|
48
|
+
if (s > 0) parts.push(`${b.name}.slice(${b.text.length - s})`);
|
|
49
|
+
expr = parts.join('+') || '""';
|
|
50
|
+
} else {
|
|
51
|
+
expr = JSON.stringify(text);
|
|
52
|
+
if (typeof text === 'string' && text.length >= MIN_SHARE) {
|
|
53
|
+
const name = `B${bases.length}`;
|
|
54
|
+
decls += `const ${name} = ${expr}\n`;
|
|
55
|
+
bases.push({ name, text });
|
|
56
|
+
expr = name;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return `${JSON.stringify(k)}: ${expr}`;
|
|
60
|
+
});
|
|
61
|
+
return { decls, expr: `{ ${entries.join(', ')} }` };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** `plan`, as source for `const PLAN = <expr>`: every field by JSON.stringify, except `prompts` (packPrompts above). */
|
|
65
|
+
function packPlan(plan) {
|
|
66
|
+
if (!plan || typeof plan !== 'object' || !plan.prompts || typeof plan.prompts !== 'object') {
|
|
67
|
+
return { decls: '', expr: JSON.stringify(plan) };
|
|
68
|
+
}
|
|
69
|
+
const { prompts, ...rest } = plan;
|
|
70
|
+
const packed = packPrompts(prompts);
|
|
71
|
+
return { decls: packed.decls, expr: `Object.assign(${JSON.stringify(rest)}, { prompts: ${packed.expr} })` };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
module.exports = { packPlan, packPrompts, sharedPrefixLen, sharedSuffixLen };
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
const gateway = require('./gateway.js');
|
|
3
|
+
const { packPlan } = require('./pack-plan.js');
|
|
3
4
|
// THE SPEC WORKFLOW (Spec → pause 2): the order of spec-mode-runner.js run() and its callees, re-expressed as a script
|
|
4
5
|
// (decisions 10, 15, 16). Every model call is an agent with its seam's definition; every judgement on an answer is the
|
|
5
6
|
// pipeline's own code, run by the hook on that answer (hooks/spec-step.js) — the script only follows the result: which
|
|
@@ -9,6 +10,7 @@ const gateway = require('./gateway.js');
|
|
|
9
10
|
|
|
10
11
|
/** The script. Plain JS, no clock, no randomness; the plan is embedded as a literal. */
|
|
11
12
|
function emit(plan) {
|
|
13
|
+
const packed = packPlan(plan);
|
|
12
14
|
return `export const meta = {
|
|
13
15
|
name: 'spec',
|
|
14
16
|
description: 'Spec stage for one lane: spec coordinator, ticket links, detective, openspec with its verification-criteria guard, change review, speckit, coordinator and model review, pause 2',
|
|
@@ -28,7 +30,7 @@ async function agentOrNull(prompt, opts) {
|
|
|
28
30
|
}
|
|
29
31
|
${gateway.textFor(plan)}
|
|
30
32
|
// GENERATED by amsd-pipeline-workflow/generator/generate-spec.js — do not edit. Source: ${plan.source}
|
|
31
|
-
const PLAN = ${
|
|
33
|
+
${packed.decls}const PLAN = ${packed.expr}
|
|
32
34
|
const A = PLAN.agents, P = PLAN.prompts, M = PLAN.models, E = PLAN.efforts, L = PLAN.limits, SCH = PLAN.schemas
|
|
33
35
|
const STEP = { type: 'object', properties: { done: { type: 'boolean' } } }
|
|
34
36
|
|
|
@@ -11,6 +11,7 @@ const fs = require('fs');
|
|
|
11
11
|
const path = require('path');
|
|
12
12
|
const { spawnSync } = require('child_process');
|
|
13
13
|
const tcs = require('./tc-slots.js');
|
|
14
|
+
const { packPlan } = require('./pack-plan.js');
|
|
14
15
|
|
|
15
16
|
const SLOT = /@@SLOT:([a-z_]+)@@/g;
|
|
16
17
|
|
|
@@ -66,6 +67,7 @@ const REVIEW_SCHEMA = { type: 'object', required: ['verdict'], properties: {
|
|
|
66
67
|
|
|
67
68
|
/** The script. Plain JS, no clock, no randomness; the plan is embedded as a literal. */
|
|
68
69
|
function emit(plan) {
|
|
70
|
+
const packed = packPlan(plan);
|
|
69
71
|
return `export const meta = {
|
|
70
72
|
name: 'writer',
|
|
71
73
|
description: 'Writer stage for one story: writer, checks (hook-owned verdict), failure analyst, model ladder, review cycle',
|
|
@@ -86,7 +88,7 @@ async function agentOrNull(prompt, opts) {
|
|
|
86
88
|
|
|
87
89
|
${gateway.textFor(plan)}
|
|
88
90
|
// GENERATED by amsd-pipeline-workflow/generator — do not edit. Source: ${plan.source}
|
|
89
|
-
const PLAN = ${
|
|
91
|
+
${packed.decls}const PLAN = ${packed.expr}
|
|
90
92
|
|
|
91
93
|
// lib/handlers/ac-apply.py reads the remediator's answer for an object with "acs" (a list of criterion texts).
|
|
92
94
|
const REMEDIATION_SCHEMA = { type: 'object', required: ['acs'], properties: { acs: { type: 'array', items: { type: 'string' } } } };
|
package/package.json
CHANGED