@miller-tech/uap 1.148.24 → 1.149.1
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/README.md +9 -0
- package/dist/.tsbuildinfo +1 -1
- package/dist/bin/cli.js +6 -3
- package/dist/bin/cli.js.map +1 -1
- package/dist/cli/deliver.js +8 -2
- package/dist/cli/deliver.js.map +1 -1
- package/dist/cli/plan.d.ts +21 -1
- package/dist/cli/plan.d.ts.map +1 -1
- package/dist/cli/plan.js +205 -14
- package/dist/cli/plan.js.map +1 -1
- package/dist/cli/verify.d.ts.map +1 -1
- package/dist/cli/verify.js +10 -1
- package/dist/cli/verify.js.map +1 -1
- package/dist/delivery/decompose.d.ts +12 -3
- package/dist/delivery/decompose.d.ts.map +1 -1
- package/dist/delivery/decompose.js +57 -4
- package/dist/delivery/decompose.js.map +1 -1
- package/dist/delivery/index.d.ts +1 -0
- package/dist/delivery/index.d.ts.map +1 -1
- package/dist/delivery/index.js +1 -0
- package/dist/delivery/index.js.map +1 -1
- package/dist/delivery/plan-check.d.ts +54 -0
- package/dist/delivery/plan-check.d.ts.map +1 -0
- package/dist/delivery/plan-check.js +165 -0
- package/dist/delivery/plan-check.js.map +1 -0
- package/dist/delivery/task-orchestrator.d.ts +47 -5
- package/dist/delivery/task-orchestrator.d.ts.map +1 -1
- package/dist/delivery/task-orchestrator.js +207 -42
- package/dist/delivery/task-orchestrator.js.map +1 -1
- package/dist/delivery/verifier-ladder.d.ts +28 -1
- package/dist/delivery/verifier-ladder.d.ts.map +1 -1
- package/dist/delivery/verifier-ladder.js +92 -4
- package/dist/delivery/verifier-ladder.js.map +1 -1
- package/docs/ATTRIBUTION.md +40 -0
- package/docs/INDEX.md +4 -0
- package/package.json +1 -1
- package/src/policies/enforcers/__pycache__/_common.cpython-312.pyc +0 -0
- package/tools/agents/scripts/__pycache__/toolcall_path_normalizer.cpython-312.pyc +0 -0
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pre-execution plan validation (the ATG-style "thought experiment").
|
|
3
|
+
*
|
|
4
|
+
* Two layers, both run BEFORE any phase executes:
|
|
5
|
+
*
|
|
6
|
+
* 1. `validatePhaseGraph` — structural: duplicate ids, unknown/self deps,
|
|
7
|
+
* dependency cycles, empty goals. Pure, no model.
|
|
8
|
+
* 2. `runPlanThoughtExperiment` — one evaluator call that mentally executes
|
|
9
|
+
* the decomposition ("will these phases, in this dependency order, deliver
|
|
10
|
+
* the mission?") and returns a pass/fail verdict with findings. The judge
|
|
11
|
+
* did not author the plan — the same generator≠evaluator separation the
|
|
12
|
+
* acceptance gate uses.
|
|
13
|
+
*
|
|
14
|
+
* `reviewPlanText` is the free-text sibling used by `uap plan validate` on
|
|
15
|
+
* markdown plan artifacts (the validate-plan-on-change gate's `validate the
|
|
16
|
+
* plan` step becomes a real review, not just a stamp).
|
|
17
|
+
*
|
|
18
|
+
* Everything is fail-soft: a validator or model error forfeits the check, it
|
|
19
|
+
* never blocks delivery — decomposition and the plan gate must never wedge.
|
|
20
|
+
*
|
|
21
|
+
* SECURITY NOTE: because of that fail-soft design (and because the judged text
|
|
22
|
+
* is authored by the same party the gate constrains), a PASS verdict is an
|
|
23
|
+
* anti-sloppiness signal, NOT an adversarial guarantee — never build
|
|
24
|
+
* enforcement that treats a PASS as proof of review.
|
|
25
|
+
*/
|
|
26
|
+
import type { LoopExecutor } from './convergence-loop.js';
|
|
27
|
+
import type { DeliveryPhase } from './decompose.js';
|
|
28
|
+
export interface PhaseGraphValidation {
|
|
29
|
+
ok: boolean;
|
|
30
|
+
errors: string[];
|
|
31
|
+
warnings: string[];
|
|
32
|
+
}
|
|
33
|
+
/** Structural validation of a phase/epic DAG. Pure — safe to call anywhere. */
|
|
34
|
+
export declare function validatePhaseGraph(phases: DeliveryPhase[]): PhaseGraphValidation;
|
|
35
|
+
export interface PlanReviewVerdict {
|
|
36
|
+
verdict: 'pass' | 'fail';
|
|
37
|
+
findings: string[];
|
|
38
|
+
}
|
|
39
|
+
/** Extract a {verdict,findings} JSON verdict from model output. Anything
|
|
40
|
+
* unparseable is a PASS — a garbled judge must not block delivery. */
|
|
41
|
+
export declare function parsePlanVerdict(text: string): PlanReviewVerdict;
|
|
42
|
+
/**
|
|
43
|
+
* ATG thought experiment: ask the evaluator to mentally execute the phase plan
|
|
44
|
+
* before anything real runs, hunting for the defects that sink multi-phase
|
|
45
|
+
* missions (missing prerequisites, wrong dependency order, phases consuming
|
|
46
|
+
* artifacts no earlier phase produces, invented scope). One call; fail-soft.
|
|
47
|
+
*/
|
|
48
|
+
export declare function runPlanThoughtExperiment(mission: string, phases: DeliveryPhase[], executor: LoopExecutor): Promise<PlanReviewVerdict>;
|
|
49
|
+
/**
|
|
50
|
+
* Free-text plan review for `uap plan validate` over a markdown plan artifact.
|
|
51
|
+
* Same verdict contract and fail-soft rules as the phase thought experiment.
|
|
52
|
+
*/
|
|
53
|
+
export declare function reviewPlanText(planText: string, executor: LoopExecutor): Promise<PlanReviewVerdict>;
|
|
54
|
+
//# sourceMappingURL=plan-check.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"plan-check.d.ts","sourceRoot":"","sources":["../../src/delivery/plan-check.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAKH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,uBAAuB,CAAC;AAC1D,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;AAEpD,MAAM,WAAW,oBAAoB;IACnC,EAAE,EAAE,OAAO,CAAC;IACZ,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,QAAQ,EAAE,MAAM,EAAE,CAAC;CACpB;AAED,+EAA+E;AAC/E,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,aAAa,EAAE,GAAG,oBAAoB,CAsChF;AAED,MAAM,WAAW,iBAAiB;IAChC,OAAO,EAAE,MAAM,GAAG,MAAM,CAAC;IACzB,QAAQ,EAAE,MAAM,EAAE,CAAC;CACpB;AAED;sEACsE;AACtE,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,iBAAiB,CAiBhE;AAWD;;;;;GAKG;AACH,wBAAsB,wBAAwB,CAC5C,OAAO,EAAE,MAAM,EACf,MAAM,EAAE,aAAa,EAAE,EACvB,QAAQ,EAAE,YAAY,GACrB,OAAO,CAAC,iBAAiB,CAAC,CA0B5B;AAED;;;GAGG;AACH,wBAAsB,cAAc,CAAC,QAAQ,EAAE,MAAM,EAAE,QAAQ,EAAE,YAAY,GAAG,OAAO,CAAC,iBAAiB,CAAC,CAyBzG"}
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pre-execution plan validation (the ATG-style "thought experiment").
|
|
3
|
+
*
|
|
4
|
+
* Two layers, both run BEFORE any phase executes:
|
|
5
|
+
*
|
|
6
|
+
* 1. `validatePhaseGraph` — structural: duplicate ids, unknown/self deps,
|
|
7
|
+
* dependency cycles, empty goals. Pure, no model.
|
|
8
|
+
* 2. `runPlanThoughtExperiment` — one evaluator call that mentally executes
|
|
9
|
+
* the decomposition ("will these phases, in this dependency order, deliver
|
|
10
|
+
* the mission?") and returns a pass/fail verdict with findings. The judge
|
|
11
|
+
* did not author the plan — the same generator≠evaluator separation the
|
|
12
|
+
* acceptance gate uses.
|
|
13
|
+
*
|
|
14
|
+
* `reviewPlanText` is the free-text sibling used by `uap plan validate` on
|
|
15
|
+
* markdown plan artifacts (the validate-plan-on-change gate's `validate the
|
|
16
|
+
* plan` step becomes a real review, not just a stamp).
|
|
17
|
+
*
|
|
18
|
+
* Everything is fail-soft: a validator or model error forfeits the check, it
|
|
19
|
+
* never blocks delivery — decomposition and the plan gate must never wedge.
|
|
20
|
+
*
|
|
21
|
+
* SECURITY NOTE: because of that fail-soft design (and because the judged text
|
|
22
|
+
* is authored by the same party the gate constrains), a PASS verdict is an
|
|
23
|
+
* anti-sloppiness signal, NOT an adversarial guarantee — never build
|
|
24
|
+
* enforcement that treats a PASS as proof of review.
|
|
25
|
+
*/
|
|
26
|
+
/** Structural validation of a phase/epic DAG. Pure — safe to call anywhere. */
|
|
27
|
+
export function validatePhaseGraph(phases) {
|
|
28
|
+
const errors = [];
|
|
29
|
+
const warnings = [];
|
|
30
|
+
const ids = new Set();
|
|
31
|
+
for (const p of phases) {
|
|
32
|
+
if (ids.has(p.id))
|
|
33
|
+
errors.push(`duplicate phase id '${p.id}'`);
|
|
34
|
+
ids.add(p.id);
|
|
35
|
+
if (!p.goal || !p.goal.trim())
|
|
36
|
+
errors.push(`phase '${p.id}' has an empty goal`);
|
|
37
|
+
}
|
|
38
|
+
for (const p of phases) {
|
|
39
|
+
for (const d of p.deps ?? []) {
|
|
40
|
+
if (d === p.id)
|
|
41
|
+
errors.push(`phase '${p.id}' depends on itself`);
|
|
42
|
+
else if (!ids.has(d))
|
|
43
|
+
warnings.push(`phase '${p.id}' depends on unknown phase '${d}' (dropped at execution)`);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
// Cycle detection (DFS with recursion stack) over known-id edges only —
|
|
47
|
+
// unknown and self deps are already reported above.
|
|
48
|
+
const visited = new Set();
|
|
49
|
+
const stack = new Set();
|
|
50
|
+
const byId = new Map(phases.map((p) => [p.id, p]));
|
|
51
|
+
const hasCycle = (id) => {
|
|
52
|
+
visited.add(id);
|
|
53
|
+
stack.add(id);
|
|
54
|
+
for (const d of byId.get(id)?.deps ?? []) {
|
|
55
|
+
if (!byId.has(d) || d === id)
|
|
56
|
+
continue;
|
|
57
|
+
if (!visited.has(d) && hasCycle(d))
|
|
58
|
+
return true;
|
|
59
|
+
if (stack.has(d))
|
|
60
|
+
return true;
|
|
61
|
+
}
|
|
62
|
+
stack.delete(id);
|
|
63
|
+
return false;
|
|
64
|
+
};
|
|
65
|
+
for (const p of phases) {
|
|
66
|
+
if (!visited.has(p.id) && hasCycle(p.id)) {
|
|
67
|
+
errors.push('dependency cycle detected — cycle members can never become ready and their subtree is skipped at execution');
|
|
68
|
+
break;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return { ok: errors.length === 0, errors, warnings };
|
|
72
|
+
}
|
|
73
|
+
/** Extract a {verdict,findings} JSON verdict from model output. Anything
|
|
74
|
+
* unparseable is a PASS — a garbled judge must not block delivery. */
|
|
75
|
+
export function parsePlanVerdict(text) {
|
|
76
|
+
try {
|
|
77
|
+
const match = text.match(/\{[\s\S]*\}/);
|
|
78
|
+
if (!match)
|
|
79
|
+
return { verdict: 'pass', findings: [] };
|
|
80
|
+
const parsed = JSON.parse(match[0]);
|
|
81
|
+
const verdict = parsed.verdict === 'fail' ? 'fail' : 'pass';
|
|
82
|
+
const findings = Array.isArray(parsed.findings)
|
|
83
|
+
? parsed.findings
|
|
84
|
+
.filter((f) => typeof f === 'string' && f.trim().length > 0)
|
|
85
|
+
// Model output reaches terminals and prompts — strip control/ANSI bytes.
|
|
86
|
+
.map((f) => f.replace(/[\x00-\x08\x0b-\x1f\x7f]/g, ' ').trim().slice(0, 300))
|
|
87
|
+
.slice(0, 8)
|
|
88
|
+
: [];
|
|
89
|
+
return { verdict, findings };
|
|
90
|
+
}
|
|
91
|
+
catch {
|
|
92
|
+
return { verdict: 'pass', findings: [] };
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
function describePhases(phases) {
|
|
96
|
+
return phases
|
|
97
|
+
.map((p, i) => `${i + 1}. [${p.id}] ${p.title} — ${p.goal}${p.deps?.length ? ` (deps: ${p.deps.join(', ')})` : ''}`)
|
|
98
|
+
.join('\n');
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* ATG thought experiment: ask the evaluator to mentally execute the phase plan
|
|
102
|
+
* before anything real runs, hunting for the defects that sink multi-phase
|
|
103
|
+
* missions (missing prerequisites, wrong dependency order, phases consuming
|
|
104
|
+
* artifacts no earlier phase produces, invented scope). One call; fail-soft.
|
|
105
|
+
*/
|
|
106
|
+
export async function runPlanThoughtExperiment(mission, phases, executor) {
|
|
107
|
+
const prompt = [
|
|
108
|
+
'You are a plan validator. Mentally EXECUTE the phase plan below, in its',
|
|
109
|
+
'dependency order, before anything real runs. You did not author this plan;',
|
|
110
|
+
'judge it adversarially.',
|
|
111
|
+
'',
|
|
112
|
+
`MISSION: ${mission.slice(0, 4000)}`,
|
|
113
|
+
'',
|
|
114
|
+
'PHASE PLAN:',
|
|
115
|
+
describePhases(phases),
|
|
116
|
+
'',
|
|
117
|
+
'Check ONLY for defects that would make execution fail or deliver the wrong thing:',
|
|
118
|
+
'- a phase that needs an artifact/decision no earlier phase (per its deps) produces',
|
|
119
|
+
'- missing prerequisite phases (setup, shared contracts, migrations)',
|
|
120
|
+
'- dependency edges that are wrong or missing (a later phase silently assumes an independent one ran first)',
|
|
121
|
+
'- phases that invent scope the mission does not imply, or mission parts no phase covers',
|
|
122
|
+
'Do NOT fail a plan for style, granularity taste, or improvements that are merely nice.',
|
|
123
|
+
'',
|
|
124
|
+
'Respond with ONLY JSON: {"verdict": "pass" | "fail", "findings": ["<specific defect>", ...]}',
|
|
125
|
+
'"fail" requires at least one concrete, execution-breaking finding.',
|
|
126
|
+
].join('\n');
|
|
127
|
+
try {
|
|
128
|
+
return parsePlanVerdict(await executor(prompt));
|
|
129
|
+
}
|
|
130
|
+
catch {
|
|
131
|
+
return { verdict: 'pass', findings: [] }; // model unavailable ⇒ check forfeited, never blocked
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Free-text plan review for `uap plan validate` over a markdown plan artifact.
|
|
136
|
+
* Same verdict contract and fail-soft rules as the phase thought experiment.
|
|
137
|
+
*/
|
|
138
|
+
export async function reviewPlanText(planText, executor) {
|
|
139
|
+
const truncated = planText.length > 12000;
|
|
140
|
+
const prompt = [
|
|
141
|
+
'You are a plan validator. Review the implementation plan below as if you',
|
|
142
|
+
'had to execute it exactly as written. You did not author it; judge it',
|
|
143
|
+
'adversarially.',
|
|
144
|
+
'',
|
|
145
|
+
'PLAN:' + (truncated ? ' (truncated to the first 12,000 characters — judge only what you see)' : ''),
|
|
146
|
+
planText.slice(0, 12000),
|
|
147
|
+
'',
|
|
148
|
+
'Check ONLY for defects that would make executing the plan fail or produce the wrong result:',
|
|
149
|
+
'- steps in an impossible order (a step consumes what a later step produces)',
|
|
150
|
+
'- missing prerequisite or verification steps for risky/irreversible actions',
|
|
151
|
+
'- load-bearing assumptions stated nowhere and likely false',
|
|
152
|
+
'- internal contradictions between sections',
|
|
153
|
+
'Do NOT fail a plan for style, brevity, or missing nice-to-haves.',
|
|
154
|
+
'',
|
|
155
|
+
'Respond with ONLY JSON: {"verdict": "pass" | "fail", "findings": ["<specific defect>", ...]}',
|
|
156
|
+
'"fail" requires at least one concrete, execution-breaking finding.',
|
|
157
|
+
].join('\n');
|
|
158
|
+
try {
|
|
159
|
+
return parsePlanVerdict(await executor(prompt));
|
|
160
|
+
}
|
|
161
|
+
catch {
|
|
162
|
+
return { verdict: 'pass', findings: [] };
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
//# sourceMappingURL=plan-check.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"plan-check.js","sourceRoot":"","sources":["../../src/delivery/plan-check.ts"],"names":[],"mappings":"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"}
|
|
@@ -86,13 +86,48 @@ export interface OrchestratorConfig {
|
|
|
86
86
|
maxTasks?: number;
|
|
87
87
|
/** Progress hook. */
|
|
88
88
|
onTask?: (task: OrchestratorTask, outcome: TaskOutcome) => void;
|
|
89
|
+
/**
|
|
90
|
+
* Minimal node repair (ATG): extra fresh attempts for a FAILED task before
|
|
91
|
+
* its dependents are blocked. Each repair attempt re-executes ONLY the
|
|
92
|
+
* failed node, with the failure summary fed into its minimal context — the
|
|
93
|
+
* validated rest of the graph stays frozen. Default: UAP_ORCH_TASK_REPAIRS
|
|
94
|
+
* env (fallback 1, env hard-ceiling 5 — a full executor rerun per attempt
|
|
95
|
+
* must not be unboundable from the environment). 0 restores fail-fast.
|
|
96
|
+
*/
|
|
97
|
+
maxRepairsPerTask?: number;
|
|
98
|
+
/**
|
|
99
|
+
* Re-plan repair (ATG minimal-subgraph repair): after retries are exhausted,
|
|
100
|
+
* re-plan the failed node as a replacement chain executed in place. The
|
|
101
|
+
* chain is interface-preserving — its first task inherits the failed node's
|
|
102
|
+
* deps, and when the whole chain succeeds the ORIGINAL task id is credited
|
|
103
|
+
* on the blackboard, so dependents unblock without any graph rewrite.
|
|
104
|
+
* Return null/[] to decline. Fail-soft: a throw declines.
|
|
105
|
+
*/
|
|
106
|
+
repairTask?: (task: OrchestratorTask, lastFailure: TaskOutcome, attempts: number) => Promise<OrchestratorTask[] | null>;
|
|
107
|
+
/**
|
|
108
|
+
* Dependency-aware parallel dispatch: max independent READY tasks running
|
|
109
|
+
* concurrently (wave-barrier scheduling: each wave completes before the
|
|
110
|
+
* next dispatches — simple and deterministic over maximal utilization).
|
|
111
|
+
* Default 1 = sequential, the historical behavior. DELIBERATELY config-only,
|
|
112
|
+
* no env override: >1 requires a runTask that tolerates concurrent
|
|
113
|
+
* execution (isolated workspaces / per-task judge state), and the
|
|
114
|
+
* production deliver runTask is NOT concurrency-safe yet — an env knob
|
|
115
|
+
* would let one exported variable silently corrupt every deliver run.
|
|
116
|
+
* Clamped to [1, 16].
|
|
117
|
+
*/
|
|
118
|
+
concurrency?: number;
|
|
89
119
|
}
|
|
90
120
|
export interface OrchestratorResult {
|
|
91
121
|
success: boolean;
|
|
122
|
+
/** Succeeded ids — includes synthetic repair-chain link ids (`x.r0-…`)
|
|
123
|
+
* alongside the credited original id when a repair chain ran. */
|
|
92
124
|
completed: string[];
|
|
93
125
|
failed: string[];
|
|
94
|
-
/** Aggregate turns across all executed tasks. */
|
|
126
|
+
/** Aggregate turns across all executed tasks (incl. retries and repairs). */
|
|
95
127
|
turns: number;
|
|
128
|
+
/** Full attempt trail: a repaired task appears MULTIPLE times (failed
|
|
129
|
+
* attempt(s), chain links, then the credited success under its original
|
|
130
|
+
* id). Consumers wanting "the" outcome for an id must take the LAST entry. */
|
|
96
131
|
outcomes: TaskOutcome[];
|
|
97
132
|
}
|
|
98
133
|
/**
|
|
@@ -114,12 +149,19 @@ export declare function governContext(prompt: string, depLineCount: number, budg
|
|
|
114
149
|
export declare function assembleTaskContext(task: OrchestratorTask, mission: string, blackboard: Map<string, TaskOutcome>, maxDepSummaryChars?: number, opts?: {
|
|
115
150
|
designLines?: string[];
|
|
116
151
|
budgetChars?: number;
|
|
152
|
+
lastFailure?: string;
|
|
117
153
|
}): AssembledContext;
|
|
118
154
|
/**
|
|
119
|
-
* P1 — execute the task DAG on a blackboard.
|
|
120
|
-
*
|
|
121
|
-
*
|
|
122
|
-
*
|
|
155
|
+
* P1 — execute the task DAG on a blackboard. Dependency-aware: a task runs
|
|
156
|
+
* only after every dependency has SUCCEEDED; independent READY tasks dispatch
|
|
157
|
+
* in parallel up to `concurrency` (default 1 — the historical sequential
|
|
158
|
+
* behavior). A task that fails gets ATG-style MINIMAL REPAIR before its
|
|
159
|
+
* dependents are blocked: bounded fresh re-execution of just that node with
|
|
160
|
+
* the failure fed back (`maxRepairsPerTask`), then an optional re-planned
|
|
161
|
+
* replacement chain (`repairTask`) credited under the original id — the
|
|
162
|
+
* validated rest of the graph stays frozen throughout. Only when repair is
|
|
163
|
+
* exhausted are dependents reported as skipped rather than run against
|
|
164
|
+
* incomplete state. Fail-soft on publish.
|
|
123
165
|
*/
|
|
124
166
|
export declare function orchestrate(config: OrchestratorConfig): Promise<OrchestratorResult>;
|
|
125
167
|
//# sourceMappingURL=task-orchestrator.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"task-orchestrator.d.ts","sourceRoot":"","sources":["../../src/delivery/task-orchestrator.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;AAGpD,wEAAwE;AACxE,MAAM,WAAW,gBAAiB,SAAQ,aAAa;IACrD,+EAA+E;IAC/E,KAAK,CAAC,EAAE,MAAM,EAAE,CAAC;IACjB,mEAAmE;IACnE,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;CACrB;AAED,4EAA4E;AAC5E,MAAM,WAAW,WAAW;IAC1B,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,OAAO,CAAC;IACjB,kFAAkF;IAClF,OAAO,EAAE,MAAM,CAAC;IAChB,kCAAkC;IAClC,KAAK,EAAE,MAAM,CAAC;IACd,0DAA0D;IAC1D,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB;;;;OAIG;IACH,QAAQ,CAAC,EAAE,gBAAgB,EAAE,CAAC;CAC/B;AAED,8DAA8D;AAC9D,MAAM,WAAW,gBAAgB;IAC/B,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,0EAA0E;IAC1E,YAAY,EAAE,MAAM,EAAE,CAAC;CACxB;AAED,MAAM,WAAW,kBAAkB;IACjC;qEACiE;IACjE,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,gBAAgB,EAAE,CAAC;IAC1B;;;;OAIG;IACH,OAAO,EAAE,CAAC,GAAG,EAAE,gBAAgB,EAAE,IAAI,EAAE,gBAAgB,KAAK,OAAO,CAAC,WAAW,CAAC,CAAC;IACjF;;;OAGG;IACH,OAAO,CAAC,EAAE,CAAC,OAAO,EAAE,WAAW,EAAE,IAAI,EAAE,gBAAgB,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACjF,2EAA2E;IAC3E,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B;;;;;OAKG;IACH,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B;;;;;OAKG;IACH,cAAc,CAAC,EAAE,CAAC,IAAI,EAAE,gBAAgB,KAAK,MAAM,EAAE,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;IAC1E,mEAAmE;IACnE,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,qBAAqB;IACrB,MAAM,CAAC,EAAE,CAAC,IAAI,EAAE,gBAAgB,EAAE,OAAO,EAAE,WAAW,KAAK,IAAI,CAAC;
|
|
1
|
+
{"version":3,"file":"task-orchestrator.d.ts","sourceRoot":"","sources":["../../src/delivery/task-orchestrator.ts"],"names":[],"mappings":"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"}
|
|
@@ -72,6 +72,12 @@ export function assembleTaskContext(task, mission, blackboard, maxDepSummaryChar
|
|
|
72
72
|
sections.push('This task is done when:');
|
|
73
73
|
task.criteria.forEach((c, i) => sections.push(` ${i + 1}. ${c}`));
|
|
74
74
|
}
|
|
75
|
+
// Minimal repair: the retry sees WHAT failed last time, not a fresh-amnesia
|
|
76
|
+
// rerun — the one new piece of information a repair attempt has.
|
|
77
|
+
if (opts.lastFailure && opts.lastFailure.trim()) {
|
|
78
|
+
sections.push('');
|
|
79
|
+
sections.push(`PREVIOUS ATTEMPT FAILED — fix this: ${opts.lastFailure.trim().slice(0, 400)}`);
|
|
80
|
+
}
|
|
75
81
|
// P3 — relevant persisted design decisions (objective/architecture): a
|
|
76
82
|
// small retrieval, not the full spec dump.
|
|
77
83
|
if (opts.designLines && opts.designLines.length > 0) {
|
|
@@ -98,41 +104,42 @@ export function assembleTaskContext(task, mission, blackboard, maxDepSummaryChar
|
|
|
98
104
|
const governed = governContext(raw, includedDeps.length, opts.budgetChars ?? DEFAULT_CONTEXT_BUDGET_CHARS);
|
|
99
105
|
return { taskId: task.id, prompt: governed.prompt, includedDeps };
|
|
100
106
|
}
|
|
107
|
+
function envInt(name, fallback, max) {
|
|
108
|
+
const rawStr = process.env[name];
|
|
109
|
+
if (!rawStr || !rawStr.trim())
|
|
110
|
+
return fallback; // empty string = unset, not 0
|
|
111
|
+
const raw = Number(rawStr);
|
|
112
|
+
return Number.isFinite(raw) && raw >= 0 ? Math.min(Math.floor(raw), max) : fallback;
|
|
113
|
+
}
|
|
101
114
|
/**
|
|
102
|
-
* P1 — execute the task DAG on a blackboard.
|
|
103
|
-
*
|
|
104
|
-
*
|
|
105
|
-
*
|
|
115
|
+
* P1 — execute the task DAG on a blackboard. Dependency-aware: a task runs
|
|
116
|
+
* only after every dependency has SUCCEEDED; independent READY tasks dispatch
|
|
117
|
+
* in parallel up to `concurrency` (default 1 — the historical sequential
|
|
118
|
+
* behavior). A task that fails gets ATG-style MINIMAL REPAIR before its
|
|
119
|
+
* dependents are blocked: bounded fresh re-execution of just that node with
|
|
120
|
+
* the failure fed back (`maxRepairsPerTask`), then an optional re-planned
|
|
121
|
+
* replacement chain (`repairTask`) credited under the original id — the
|
|
122
|
+
* validated rest of the graph stays frozen throughout. Only when repair is
|
|
123
|
+
* exhausted are dependents reported as skipped rather than run against
|
|
124
|
+
* incomplete state. Fail-soft on publish.
|
|
106
125
|
*/
|
|
107
126
|
export async function orchestrate(config) {
|
|
108
127
|
const maxTasks = config.maxTasks ?? DEFAULT_MAX_TASKS;
|
|
109
128
|
const budget = config.contextBudgetChars;
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
129
|
+
const concurrency = Math.min(Math.max(1, config.concurrency ?? 1), 16);
|
|
130
|
+
const maxRepairs = Math.max(0, config.maxRepairsPerTask ?? envInt('UAP_ORCH_TASK_REPAIRS', 1, 5));
|
|
131
|
+
// Pending is kept topo-ordered; P5 re-planning appends discovered subtasks
|
|
132
|
+
// and re-sorts. `scheduled` counts every task ever admitted (initial +
|
|
133
|
+
// re-planned + repair chains) against the maxTasks runaway guard.
|
|
134
|
+
let pending = topoOrder(config.tasks);
|
|
135
|
+
const known = new Set(pending.map((t) => t.id));
|
|
136
|
+
let scheduled = pending.length;
|
|
114
137
|
const blackboard = new Map();
|
|
115
138
|
const done = new Set();
|
|
116
139
|
const failed = new Set();
|
|
117
140
|
const outcomes = [];
|
|
118
141
|
let turns = 0;
|
|
119
|
-
|
|
120
|
-
const task = queue[qi];
|
|
121
|
-
const deps = task.deps ?? [];
|
|
122
|
-
// Skip a task whose dependency failed — never build against a broken base.
|
|
123
|
-
const blockedBy = deps.filter((d) => failed.has(d) || !done.has(d));
|
|
124
|
-
if (blockedBy.length > 0) {
|
|
125
|
-
const outcome = {
|
|
126
|
-
taskId: task.id,
|
|
127
|
-
success: false,
|
|
128
|
-
summary: `skipped — unmet dependency: ${blockedBy.join(', ')}`,
|
|
129
|
-
turns: 0,
|
|
130
|
-
};
|
|
131
|
-
failed.add(task.id);
|
|
132
|
-
outcomes.push(outcome);
|
|
133
|
-
config.onTask?.(task, outcome);
|
|
134
|
-
continue;
|
|
135
|
-
}
|
|
142
|
+
const runOne = async (task, lastFailure) => {
|
|
136
143
|
let designLines = [];
|
|
137
144
|
if (config.retrieveDesign) {
|
|
138
145
|
try {
|
|
@@ -145,34 +152,192 @@ export async function orchestrate(config) {
|
|
|
145
152
|
const ctx = assembleTaskContext(task, config.mission, blackboard, config.maxDepSummaryChars, {
|
|
146
153
|
designLines,
|
|
147
154
|
budgetChars: budget,
|
|
155
|
+
...(lastFailure ? { lastFailure } : {}),
|
|
148
156
|
});
|
|
149
|
-
|
|
157
|
+
// A throwing runTask becomes a failed outcome, never an abandoned graph:
|
|
158
|
+
// under parallel dispatch one rejection must not orphan in-flight siblings.
|
|
159
|
+
let outcome;
|
|
160
|
+
try {
|
|
161
|
+
outcome = await config.runTask(ctx, task);
|
|
162
|
+
}
|
|
163
|
+
catch (err) {
|
|
164
|
+
outcome = {
|
|
165
|
+
taskId: task.id,
|
|
166
|
+
success: false,
|
|
167
|
+
summary: `task execution error: ${err instanceof Error ? err.message : String(err)}`.slice(0, 300),
|
|
168
|
+
turns: 0,
|
|
169
|
+
};
|
|
170
|
+
}
|
|
150
171
|
turns += outcome.turns;
|
|
151
172
|
outcomes.push(outcome);
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
173
|
+
return outcome;
|
|
174
|
+
};
|
|
175
|
+
const recordSuccess = async (id, task, outcome) => {
|
|
176
|
+
done.add(id);
|
|
177
|
+
blackboard.set(id, outcome);
|
|
178
|
+
try {
|
|
179
|
+
await config.publish?.(outcome, task);
|
|
180
|
+
}
|
|
181
|
+
catch {
|
|
182
|
+
// publishing to durable memory is best-effort
|
|
183
|
+
}
|
|
184
|
+
};
|
|
185
|
+
const markSkipped = (task, blockedBy) => {
|
|
186
|
+
const outcome = {
|
|
187
|
+
taskId: task.id,
|
|
188
|
+
success: false,
|
|
189
|
+
summary: `skipped — unmet dependency: ${blockedBy.join(', ')}`,
|
|
190
|
+
turns: 0,
|
|
191
|
+
};
|
|
192
|
+
failed.add(task.id);
|
|
193
|
+
outcomes.push(outcome);
|
|
194
|
+
config.onTask?.(task, outcome);
|
|
195
|
+
};
|
|
196
|
+
/**
|
|
197
|
+
* Re-plan repair: execute the replacement chain in place. Interface
|
|
198
|
+
* preserving — the first link inherits the failed node's deps, links chain
|
|
199
|
+
* sequentially, and full-chain success credits the ORIGINAL task id on the
|
|
200
|
+
* blackboard so dependents unblock unchanged. Returns the credited outcome,
|
|
201
|
+
* or null when the repair was declined or the chain broke.
|
|
202
|
+
*/
|
|
203
|
+
const runRepairChain = async (task, lastFailure, attempts) => {
|
|
204
|
+
if (!config.repairTask)
|
|
205
|
+
return null;
|
|
206
|
+
let plan = null;
|
|
207
|
+
try {
|
|
208
|
+
plan = await config.repairTask(task, lastFailure, attempts);
|
|
209
|
+
}
|
|
210
|
+
catch {
|
|
211
|
+
plan = null; // fail-soft: an unplannable repair keeps the failure
|
|
212
|
+
}
|
|
213
|
+
if (!plan || plan.length === 0 || scheduled + plan.length > maxTasks)
|
|
214
|
+
return null;
|
|
215
|
+
let prevId;
|
|
216
|
+
let last = null;
|
|
217
|
+
for (let i = 0; i < plan.length; i++) {
|
|
218
|
+
// Namespace chain links under the failed node. The 64-char cut can eat
|
|
219
|
+
// the differentiator for very long task ids, so collisions against
|
|
220
|
+
// anything ever known get a numeric suffix instead of silently
|
|
221
|
+
// overwriting a blackboard entry.
|
|
222
|
+
let subId = `${task.id}.r${i}-${plan[i].id}`.slice(0, 64);
|
|
223
|
+
for (let bump = 2; known.has(subId); bump++)
|
|
224
|
+
subId = `${subId.slice(0, 60)}~${bump}`;
|
|
225
|
+
const sub = {
|
|
226
|
+
...plan[i],
|
|
227
|
+
id: subId,
|
|
228
|
+
deps: i === 0 ? [...(task.deps ?? [])] : [prevId],
|
|
229
|
+
};
|
|
230
|
+
known.add(sub.id);
|
|
231
|
+
scheduled++;
|
|
232
|
+
// Chain links run to plan; their own newTasks are DELIBERATELY dropped —
|
|
233
|
+
// a repair must converge the failed node, not grow the graph.
|
|
234
|
+
const outcome = await runOne(sub, i === 0 ? lastFailure.summary : undefined);
|
|
235
|
+
config.onTask?.(sub, outcome);
|
|
236
|
+
if (!outcome.success) {
|
|
237
|
+
failed.add(sub.id);
|
|
238
|
+
return null;
|
|
160
239
|
}
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
240
|
+
await recordSuccess(sub.id, sub, outcome);
|
|
241
|
+
prevId = sub.id;
|
|
242
|
+
last = outcome;
|
|
243
|
+
}
|
|
244
|
+
if (!last)
|
|
245
|
+
return null;
|
|
246
|
+
const credited = {
|
|
247
|
+
taskId: task.id,
|
|
248
|
+
success: true,
|
|
249
|
+
summary: `repaired via ${plan.length} replacement task(s): ${last.summary}`.slice(0, 400),
|
|
250
|
+
turns: 0,
|
|
251
|
+
...(last.contract ? { contract: last.contract } : {}),
|
|
252
|
+
};
|
|
253
|
+
outcomes.push(credited);
|
|
254
|
+
await recordSuccess(task.id, task, credited);
|
|
255
|
+
return credited;
|
|
256
|
+
};
|
|
257
|
+
/** Full failure handling for one task: retries → re-plan chain → fail. */
|
|
258
|
+
const settleTask = async (task, first) => {
|
|
259
|
+
let final = first;
|
|
260
|
+
let attempts = 1;
|
|
261
|
+
// Minimal repair 1 — bounded re-execution of JUST this node, failure fed back.
|
|
262
|
+
while (!final.success && attempts <= maxRepairs) {
|
|
263
|
+
final = await runOne(task, final.summary);
|
|
264
|
+
attempts++;
|
|
265
|
+
}
|
|
266
|
+
// Minimal repair 2 — re-planned replacement chain, original id credited.
|
|
267
|
+
if (!final.success) {
|
|
268
|
+
const credited = await runRepairChain(task, final, attempts);
|
|
269
|
+
if (credited)
|
|
270
|
+
final = credited;
|
|
271
|
+
}
|
|
272
|
+
if (final.success) {
|
|
273
|
+
// Repair-chain success already recorded itself under the original id.
|
|
274
|
+
if (!done.has(task.id))
|
|
275
|
+
await recordSuccess(task.id, task, final);
|
|
276
|
+
// P5 — adaptive re-planning: fold discovered subtasks into the graph.
|
|
277
|
+
const fresh = (final.newTasks ?? []).filter((t) => !known.has(t.id));
|
|
278
|
+
if (fresh.length > 0 && scheduled + fresh.length <= maxTasks) {
|
|
164
279
|
for (const t of fresh)
|
|
165
280
|
known.add(t.id);
|
|
166
|
-
|
|
167
|
-
const
|
|
168
|
-
const
|
|
169
|
-
|
|
281
|
+
scheduled += fresh.length;
|
|
282
|
+
const merged = pending.concat(fresh);
|
|
283
|
+
const sorted = topoOrder(merged);
|
|
284
|
+
// topoOrder cleans deps against ONLY the passed array — which would
|
|
285
|
+
// silently drop edges to already-done (harmless) and already-FAILED
|
|
286
|
+
// (dangerous: dependents would run on a broken base) tasks. Restore
|
|
287
|
+
// each task's true deps, filtered against everything ever known.
|
|
288
|
+
const byId = new Map(merged.map((t) => [t.id, t]));
|
|
289
|
+
pending = sorted.map((s) => {
|
|
290
|
+
const orig = byId.get(s.id);
|
|
291
|
+
const deps = (orig.deps ?? []).filter((d) => known.has(d) && d !== s.id);
|
|
292
|
+
return { ...orig, deps };
|
|
293
|
+
});
|
|
170
294
|
}
|
|
171
295
|
}
|
|
172
296
|
else {
|
|
173
297
|
failed.add(task.id);
|
|
174
298
|
}
|
|
175
|
-
config.onTask?.(task,
|
|
299
|
+
config.onTask?.(task, final);
|
|
300
|
+
return final;
|
|
301
|
+
};
|
|
302
|
+
while (pending.length > 0) {
|
|
303
|
+
// Skip cascade: drop every task whose dependency already failed — never
|
|
304
|
+
// build against a broken base. Loops because each skip can cascade.
|
|
305
|
+
let cascading = true;
|
|
306
|
+
while (cascading) {
|
|
307
|
+
cascading = false;
|
|
308
|
+
for (let i = 0; i < pending.length; i++) {
|
|
309
|
+
const blockedBy = (pending[i].deps ?? []).filter((d) => failed.has(d));
|
|
310
|
+
if (blockedBy.length > 0) {
|
|
311
|
+
markSkipped(pending[i], blockedBy);
|
|
312
|
+
pending.splice(i, 1);
|
|
313
|
+
i--;
|
|
314
|
+
cascading = true;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
if (pending.length === 0)
|
|
319
|
+
break;
|
|
320
|
+
// Ready set: every dependency has SUCCEEDED. Nothing ready with work
|
|
321
|
+
// still pending means an unsatisfiable remainder (cycle degraded by
|
|
322
|
+
// topoOrder) — skip it rather than wedge.
|
|
323
|
+
const ready = pending.filter((t) => (t.deps ?? []).every((d) => done.has(d)));
|
|
324
|
+
if (ready.length === 0) {
|
|
325
|
+
for (const t of pending)
|
|
326
|
+
markSkipped(t, (t.deps ?? []).filter((d) => !done.has(d)));
|
|
327
|
+
pending = [];
|
|
328
|
+
break;
|
|
329
|
+
}
|
|
330
|
+
// Dependency-aware dispatch: up to `concurrency` independent ready tasks
|
|
331
|
+
// run at once (they are mutually independent by construction — none can
|
|
332
|
+
// depend on another still-pending task). Settlement (repair, re-planning,
|
|
333
|
+
// bookkeeping) is sequential for deterministic graph state.
|
|
334
|
+
const wave = ready.slice(0, concurrency);
|
|
335
|
+
const waveIds = new Set(wave.map((t) => t.id));
|
|
336
|
+
pending = pending.filter((t) => !waveIds.has(t.id));
|
|
337
|
+
const results = await Promise.all(wave.map(async (t) => ({ task: t, outcome: await runOne(t) })));
|
|
338
|
+
for (const { task, outcome } of results) {
|
|
339
|
+
await settleTask(task, outcome);
|
|
340
|
+
}
|
|
176
341
|
}
|
|
177
342
|
return {
|
|
178
343
|
success: failed.size === 0,
|