create-agdf 0.13.1 → 0.13.3

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.
Files changed (78) hide show
  1. package/generated/.agdf/control/templates/RUN_STATE.md +22 -0
  2. package/generated/.agdf/control/templates/artefacts/OR.md +22 -0
  3. package/generated/.github/skills/agdf-delivery-closeout/SKILL.md +7 -0
  4. package/generated/.github/skills/agdf-interaction-locales.json +2 -4
  5. package/generated/.github/skills/agdf-release-or/SKILL.md +14 -4
  6. package/generated/.github/skills/contracts/closeout.md +49 -0
  7. package/generated/.github/skills/contracts/control-scaffold.md +2 -0
  8. package/generated/.github/skills/contracts/interaction.md +23 -12
  9. package/generated/.opencode/agdf-interaction-locales.json +2 -4
  10. package/generated/.opencode/contracts/closeout.md +49 -0
  11. package/generated/.opencode/contracts/control-scaffold.md +2 -0
  12. package/generated/.opencode/contracts/interaction.md +23 -12
  13. package/generated/.opencode/skills/agdf-delivery-closeout/SKILL.md +7 -0
  14. package/generated/.opencode/skills/agdf-release-or/SKILL.md +14 -4
  15. package/generated/plugins/agdf/.claude-plugin/plugin.json +1 -1
  16. package/generated/plugins/agdf/.codex-plugin/plugin.json +1 -1
  17. package/generated/plugins/agdf/control/templates/RUN_STATE.md +22 -0
  18. package/generated/plugins/agdf/control/templates/artefacts/OR.md +22 -0
  19. package/generated/plugins/agdf/meta/agdf-interaction-locales.json +2 -4
  20. package/generated/plugins/agdf/meta/agdf-plugin.definition.json +1 -1
  21. package/generated/plugins/agdf/meta/agent-skills-conformance.json +86 -0
  22. package/generated/plugins/agdf/meta/contracts/closeout.md +49 -0
  23. package/generated/plugins/agdf/meta/contracts/control-scaffold.md +2 -0
  24. package/generated/plugins/agdf/meta/contracts/interaction.md +23 -12
  25. package/generated/plugins/agdf/runtime/create-agdf/generated/plugins/agdf/meta/agdf-interaction-locales.json +2 -4
  26. package/generated/plugins/agdf/runtime/create-agdf/generated/plugins/agdf/meta/agdf-plugin.definition.json +1 -1
  27. package/generated/plugins/agdf/runtime/create-agdf/lib/control-evaluation/delivery-map.js +13 -3
  28. package/generated/plugins/agdf/runtime/create-agdf/lib/control-evaluation/doctor.js +9 -2
  29. package/generated/plugins/agdf/runtime/create-agdf/lib/control-evaluation/gate-check.js +6 -1
  30. package/generated/plugins/agdf/runtime/create-agdf/lib/control-evaluation/gate-policy.js +16 -1
  31. package/generated/plugins/agdf/runtime/create-agdf/lib/control-evaluation/parent-reconciliation.js +284 -0
  32. package/generated/plugins/agdf/runtime/create-agdf/lib/control-state/run-state-parser.js +39 -1
  33. package/generated/plugins/agdf/runtime/create-agdf/lib/interaction-presentation.js +31 -14
  34. package/generated/plugins/agdf/runtime/create-agdf/package.json +1 -1
  35. package/generated/plugins/agdf/runtime/runtime-manifest.json +2 -2
  36. package/generated/plugins/agdf/scripts/agent-skills-conformance.mjs +543 -0
  37. package/generated/plugins/agdf/scripts/check-runtime-integrity.mjs +31 -17
  38. package/generated/plugins/agdf/skills/delivery-closeout/SKILL.md +7 -0
  39. package/generated/plugins/agdf/skills/release-or/SKILL.md +14 -4
  40. package/generated/plugins/agdf/submission/openai/availability.md +1 -1
  41. package/generated/plugins/agdf/submission/openai/capability-matrix.json +2 -2
  42. package/generated/plugins/agdf/submission/openai/release-notes.md +1 -1
  43. package/generated/plugins/agdf/submission/openai/reviewer-cases.json +1 -1
  44. package/generated/submissions/openai/agdf/.codex-plugin/plugin.json +1 -1
  45. package/generated/submissions/openai/agdf/meta/agdf-interaction-locales.json +2 -4
  46. package/generated/submissions/openai/agdf/meta/agdf-plugin.definition.json +1 -1
  47. package/generated/submissions/openai/agdf/meta/agent-skills-conformance.json +86 -0
  48. package/generated/submissions/openai/agdf/meta/contracts/closeout.md +49 -0
  49. package/generated/submissions/openai/agdf/meta/contracts/control-scaffold.md +2 -0
  50. package/generated/submissions/openai/agdf/meta/contracts/interaction.md +23 -12
  51. package/generated/submissions/openai/agdf/skills/delivery-closeout/SKILL.md +7 -0
  52. package/generated/submissions/openai/agdf/skills/release-or/SKILL.md +14 -4
  53. package/generated/submissions/openai/agdf/submission/openai/availability.md +1 -1
  54. package/generated/submissions/openai/agdf/submission/openai/capability-matrix.json +2 -2
  55. package/generated/submissions/openai/agdf/submission/openai/inventory.json +19 -15
  56. package/generated/submissions/openai/agdf/submission/openai/listing.json +1 -1
  57. package/generated/submissions/openai/agdf/submission/openai/readiness.json +2 -2
  58. package/generated/submissions/openai/agdf/submission/openai/readiness.md +2 -2
  59. package/generated/submissions/openai/agdf/submission/openai/release-notes.md +1 -1
  60. package/generated/submissions/openai/agdf/submission/openai/reviewer-cases.json +1 -1
  61. package/lib/control-evaluation/delivery-map.js +13 -3
  62. package/lib/control-evaluation/doctor.js +9 -2
  63. package/lib/control-evaluation/gate-check.js +6 -1
  64. package/lib/control-evaluation/gate-policy.js +16 -1
  65. package/lib/control-evaluation/parent-reconciliation.js +284 -0
  66. package/lib/control-state/run-state-parser.js +39 -1
  67. package/lib/interaction-presentation.js +31 -14
  68. package/lib/proportionality-benchmark/blind-prompt.js +2 -1
  69. package/lib/proportionality-benchmark/contracts.js +16 -7
  70. package/lib/proportionality-benchmark/corpus-loader.js +132 -21
  71. package/lib/proportionality-benchmark/evaluator.js +25 -15
  72. package/lib/proportionality-benchmark/index.js +1 -0
  73. package/lib/proportionality-benchmark/live-recorder.js +32 -10
  74. package/lib/proportionality-benchmark/profiles.js +102 -0
  75. package/lib/proportionality-benchmark/report.js +10 -1
  76. package/lib/proportionality-benchmark/source-fingerprint.js +6 -2
  77. package/lib/release/version-coherence.js +79 -0
  78. package/package.json +7 -3
@@ -3,8 +3,9 @@ import { join } from 'node:path';
3
3
  import { aggregate, resolveRuns } from '../control-state/index.js';
4
4
  import { resolveConfiguredChatLanguage } from '../cli/runtime-context.js';
5
5
  import { isGateSatisfied, transitionDecisionForRunState } from './gate-policy.js';
6
- import { gateApprovalStatus, readRunState } from './run-state.js';
6
+ import { gateApprovalStatus, readRunState, resolvedArtefactFile } from './run-state.js';
7
7
  import { allowNoActiveRuns, filled, isPlaceholderValue, markdownSection, parseBacklogSection, readTargetFile } from './shared.js';
8
+ import { evaluateReconciliationState } from './parent-reconciliation.js';
8
9
 
9
10
  const deliveryRelationships = [
10
11
  { from: "UR", relationship: "approved_by", to: "Approval: UR", requiredBy: "UR" },
@@ -38,8 +39,10 @@ function severityFromImpact(value) {
38
39
  return null;
39
40
  }
40
41
 
41
- export function analyzeDeliveryMap(runState) {
42
+ export function analyzeDeliveryMap(runState, dependencies = {}) {
42
43
  const findings = [];
44
+ const reconciliation = evaluateReconciliationState(runState, dependencies);
45
+ findings.push(...reconciliation.findings);
43
46
  const relationships = deliveryRelationships.map((expected) => {
44
47
  const row = findRelationship(runState, expected);
45
48
  const required = relationshipRequired(runState, expected.requiredBy);
@@ -142,6 +145,8 @@ export function analyzeDeliveryMap(runState) {
142
145
  context_graph: runState.context_graph,
143
146
  source_scope: runState.source_scope,
144
147
  memory: runState.memory,
148
+ parent_reconciliation: reconciliation.parent_reconciliation,
149
+ programme_aggregation: reconciliation.programme_aggregation,
145
150
  findings,
146
151
  };
147
152
  }
@@ -215,7 +220,10 @@ export function evaluateDeliveryMap(targetDir, selection = {}, dependencies = {}
215
220
  }
216
221
  const doctorReport = evaluateDoctor(targetDir, selection);
217
222
  const runState = readRunState(targetDir, selection);
218
- const map = analyzeDeliveryMap(runState);
223
+ const map = analyzeDeliveryMap(runState, {
224
+ loadRun: (runId) => readRunState(targetDir, { runId }),
225
+ resolveFile: (path) => resolvedArtefactFile(targetDir, path),
226
+ });
219
227
  const gateDecision = transitionDecisionForRunState(runState);
220
228
  const currentGate = gateDecision.current_gate;
221
229
 
@@ -262,6 +270,8 @@ export function evaluateDeliveryMap(targetDir, selection = {}, dependencies = {}
262
270
  context_graph: map.context_graph,
263
271
  source_scope: map.source_scope,
264
272
  memory: map.memory,
273
+ parent_reconciliation: map.parent_reconciliation,
274
+ programme_aggregation: map.programme_aggregation,
265
275
  findings: map.findings,
266
276
  doctor_status: doctorReport.status,
267
277
  doctor_summary: doctorReport.summary,
@@ -4,7 +4,7 @@ import { aggregate, resolveRuns, verifyLegacyProjection } from '../control-state
4
4
  import { doctorRequiredFiles } from './required-files.js';
5
5
  import { analyzeDeliveryMap } from './delivery-map.js';
6
6
  import { evaluateVerifiedChange } from './verified-change.js';
7
- import { analyzeArtefactRoleConsistency, analyzeDurableGateArtefactConsistency, modeSliceDecision, readRunState } from './run-state.js';
7
+ import { analyzeArtefactRoleConsistency, analyzeDurableGateArtefactConsistency, modeSliceDecision, readRunState, resolvedArtefactFile } from './run-state.js';
8
8
  import { addFinding, allowNoActiveRuns, filled, hasFilledEvidenceRow, hasFilledTableRow, isPlaceholderValue, markdownSection, nonEmptyTableRows, parseBacklogSection, parseQualityContracts, readTargetFile, runSelectionRecovery, tableRows } from './shared.js';
9
9
 
10
10
  export function evaluateDoctor(targetDir, selection = {}) {
@@ -45,6 +45,7 @@ export function evaluateDoctor(targetDir, selection = {}) {
45
45
  };
46
46
  }
47
47
  const findings = [];
48
+ let reconciliation;
48
49
  const hasCanonicalRuns = existsSync(join(targetDir, ".agdf", "control", "runs"));
49
50
  if (hasCanonicalRuns) {
50
51
  const projection = verifyLegacyProjection(targetDir);
@@ -221,7 +222,11 @@ export function evaluateDoctor(targetDir, selection = {}) {
221
222
  for (const finding of analyzeArtefactRoleConsistency(targetDir, runState)) {
222
223
  addFinding(findings, finding.severity, finding.code, finding.message, finding.path, finding.next_step);
223
224
  }
224
- for (const finding of analyzeDeliveryMap(runState).findings) {
225
+ reconciliation = analyzeDeliveryMap(runState, {
226
+ loadRun: (runId) => readRunState(targetDir, { runId }),
227
+ resolveFile: (path) => resolvedArtefactFile(targetDir, path),
228
+ });
229
+ for (const finding of reconciliation.findings) {
225
230
  addFinding(findings, finding.severity, finding.code, finding.message, finding.path, finding.next_step);
226
231
  }
227
232
  }
@@ -241,6 +246,8 @@ export function evaluateDoctor(targetDir, selection = {}) {
241
246
  revise: findings.filter((finding) => finding.severity === "revise").length,
242
247
  warn: findings.filter((finding) => finding.severity === "warn").length,
243
248
  },
249
+ parent_reconciliation: reconciliation?.parent_reconciliation,
250
+ programme_aggregation: reconciliation?.programme_aggregation,
244
251
  findings,
245
252
  };
246
253
  }
@@ -225,7 +225,10 @@ export function evaluateGateCheck(targetDir, selection = {}) {
225
225
  ? evaluateVerifiedChange(targetDir, runState)
226
226
  : null;
227
227
  const transitionDecision = transitionDecisionForRunState(runState, verifiedChange);
228
- const deliveryMap = analyzeDeliveryMap(runState);
228
+ const deliveryMap = analyzeDeliveryMap(runState, {
229
+ loadRun: (runId) => readRunState(targetDir, { runId }),
230
+ resolveFile: (path) => resolvedArtefactFile(targetDir, path),
231
+ });
229
232
  const doctorBlocker = doctorReport.findings.find((finding) => finding.severity === "block");
230
233
  const doctorRevise = doctorReport.findings.find((finding) => finding.severity === "revise");
231
234
  const routesInvalidVerifiedChange = modeSliceDecision(runState) === "verified_change"
@@ -344,6 +347,8 @@ export function evaluateGateCheck(targetDir, selection = {}) {
344
347
  context_graph: deliveryMap.context_graph,
345
348
  source_scope: deliveryMap.source_scope,
346
349
  memory: deliveryMap.memory,
350
+ parent_reconciliation: deliveryMap.parent_reconciliation,
351
+ programme_aggregation: deliveryMap.programme_aggregation,
347
352
  findings: deliveryMap.findings,
348
353
  },
349
354
  verified_change: verifiedChange,
@@ -1,4 +1,4 @@
1
- import { verifiedChangeEscalationTargets } from './verified-change.js';
1
+ import { extractField, verifiedChangeEscalationTargets } from './verified-change.js';
2
2
  import { gateApprovalStatus, gateArtefactStatus, isDurableGateArtefactSatisfied, isInternalStepSatisfied, modeSliceDecision } from './run-state.js';
3
3
 
4
4
  function durableArtefactBlock(gate, nextGate) {
@@ -98,6 +98,21 @@ export function transitionDecisionForRunState(runState, verifiedChange = null) {
98
98
  }
99
99
 
100
100
  if (modeDecision === "quick_task") {
101
+ if (
102
+ extractField(runState.content ?? "", "lifecycle") === "completed"
103
+ && runState.current_gate === "OR"
104
+ && runState.artefacts.get("OR")?.status === "done"
105
+ ) {
106
+ return {
107
+ status: "pass",
108
+ current_gate: "OR",
109
+ blocking_reason: "none",
110
+ missing_approval: "none",
111
+ allowed: ["use delivery closeout when explicitly requested"],
112
+ forbidden: ["repeat Quick Task execution", "commit, push, open PR or release automatically"],
113
+ next_allowed_action: "Use delivery closeout only when an operative VCS handoff is explicitly requested.",
114
+ };
115
+ }
101
116
  return {
102
117
  status: "open",
103
118
  current_gate: "Quick Task Execution",
@@ -0,0 +1,284 @@
1
+ import { RUN_ID_PATTERN } from "../control-state/run-state-parser.js";
2
+
3
+ const parentPrefix = "parent_run:";
4
+ const childPrefix = "child_run:";
5
+
6
+ function filled(value) {
7
+ const text = String(value ?? "").trim();
8
+ return Boolean(text && text !== "none" && !text.includes("<"));
9
+ }
10
+
11
+ function exactTarget(value, prefix) {
12
+ const text = String(value ?? "").trim();
13
+ if (!text.startsWith(prefix)) return "";
14
+ const runId = text.slice(prefix.length);
15
+ return RUN_ID_PATTERN.test(runId) && `${prefix}${runId}` === text ? runId : "";
16
+ }
17
+
18
+ function warning(runState, code, message, nextStep) {
19
+ return {
20
+ severity: "warn",
21
+ code,
22
+ message,
23
+ path: runState.path,
24
+ next_step: nextStep,
25
+ };
26
+ }
27
+
28
+ function invalidDispositionInput(runState) {
29
+ const input = runState.parent_reconciliation ?? {};
30
+ if (!input.present) return false;
31
+ const counts = input.field_counts ?? {};
32
+ return counts.disposition !== 1
33
+ || counts.next_action !== 1
34
+ || !["action_required", "accepted_open"].includes(input.disposition)
35
+ || !filled(input.next_action);
36
+ }
37
+
38
+ function openResult(runState, {
39
+ code = "AGDF_PARENT_RECONCILIATION_OPEN",
40
+ targetRunId = "",
41
+ evidence = "",
42
+ missingEvidence,
43
+ nextAction,
44
+ } = {}) {
45
+ const input = runState.parent_reconciliation ?? {};
46
+ const fieldCounts = input.field_counts ?? {};
47
+ const validDisposition = ["action_required", "accepted_open"].includes(input.disposition)
48
+ && fieldCounts.disposition === 1;
49
+ const validNextAction = filled(input.next_action) && fieldCounts.next_action === 1;
50
+ const declaredInputInvalid = input.present && (!validDisposition || !validNextAction);
51
+ const action = declaredInputInvalid ? nextAction : validNextAction ? input.next_action : nextAction;
52
+ const finalCode = declaredInputInvalid
53
+ ? "AGDF_PARENT_RECONCILIATION_EVIDENCE_INVALID"
54
+ : code;
55
+ return {
56
+ result: {
57
+ outcome: "open",
58
+ target_run_id: targetRunId,
59
+ disposition: validDisposition ? input.disposition : "action_required",
60
+ evidence,
61
+ missing_evidence: missingEvidence,
62
+ next_action: action,
63
+ },
64
+ findings: [warning(
65
+ runState,
66
+ finalCode,
67
+ targetRunId
68
+ ? `Child ${runState.run_id || "run"} has an open reconciliation handoff to Parent ${targetRunId}.`
69
+ : `Child ${runState.run_id || "run"} has invalid or ambiguous explicit Parent relationship evidence.`,
70
+ action,
71
+ )],
72
+ };
73
+ }
74
+
75
+ export function evaluateParentReconciliation(runState, { loadRun } = {}) {
76
+ const rows = runState.artefact_chain.filter((row) =>
77
+ row.from === "OR" && row.relationship === "reconciles_with"
78
+ );
79
+
80
+ if (!rows.length) {
81
+ return {
82
+ result: {
83
+ outcome: "not_applicable",
84
+ target_run_id: "",
85
+ disposition: "not_applicable",
86
+ evidence: "",
87
+ missing_evidence: "none",
88
+ next_action: "none",
89
+ },
90
+ findings: [],
91
+ };
92
+ }
93
+
94
+ if (rows.length !== 1) {
95
+ return openResult(runState, {
96
+ code: "AGDF_PARENT_RECONCILIATION_EVIDENCE_INVALID",
97
+ missingEvidence: "Exactly one explicit Parent relationship is required.",
98
+ nextAction: "Retain exactly one evidenced OR reconciles_with parent_run:<run_id> relationship.",
99
+ });
100
+ }
101
+
102
+ if (invalidDispositionInput(runState)) {
103
+ return openResult(runState, {
104
+ code: "AGDF_PARENT_RECONCILIATION_EVIDENCE_INVALID",
105
+ missingEvidence: "The Parent reconciliation disposition or next action is invalid or duplicated.",
106
+ nextAction: "Record one valid disposition and exactly one concrete Parent reconciliation next action.",
107
+ });
108
+ }
109
+
110
+ const row = rows[0];
111
+ const parentRunId = exactTarget(row.to, parentPrefix);
112
+ if (!parentRunId || parentRunId === runState.run_id || !filled(row.evidence)) {
113
+ return openResult(runState, {
114
+ code: "AGDF_PARENT_RECONCILIATION_EVIDENCE_INVALID",
115
+ targetRunId: parentRunId,
116
+ evidence: row.evidence,
117
+ missingEvidence: "The Parent run ID and concrete relationship evidence must be valid and unambiguous.",
118
+ nextAction: "Repair the explicit OR reconciles_with parent_run:<run_id> row and its evidence.",
119
+ });
120
+ }
121
+
122
+ let parent;
123
+ try {
124
+ parent = loadRun?.(parentRunId);
125
+ } catch {
126
+ parent = null;
127
+ }
128
+ if (!parent?.content || parent.resolution_error) {
129
+ return openResult(runState, {
130
+ targetRunId: parentRunId,
131
+ evidence: row.evidence,
132
+ missingEvidence: "The named Parent run is unavailable.",
133
+ nextAction: `Restore or select Parent run ${parentRunId}, then re-run Delivery Map.`,
134
+ });
135
+ }
136
+
137
+ const reciprocalRows = parent.artefact_chain.filter((candidate) =>
138
+ candidate.from === "Aggregate"
139
+ && candidate.relationship === "includes"
140
+ && candidate.to === `${childPrefix}${runState.run_id}`
141
+ );
142
+ if (reciprocalRows.length !== 1 || !filled(reciprocalRows[0]?.evidence)) {
143
+ return openResult(runState, {
144
+ targetRunId: parentRunId,
145
+ evidence: row.evidence,
146
+ missingEvidence: reciprocalRows.length > 1
147
+ ? "The Parent contains ambiguous reciprocal Child relationships."
148
+ : "The Parent lacks one evidenced reciprocal Aggregate includes Child relationship.",
149
+ nextAction: `Reconcile Child ${runState.run_id} in Parent ${parentRunId} through the Parent's governed scope.`,
150
+ });
151
+ }
152
+
153
+ return {
154
+ result: {
155
+ outcome: "resolved",
156
+ target_run_id: parentRunId,
157
+ disposition: "not_applicable",
158
+ evidence: reciprocalRows[0].evidence,
159
+ missing_evidence: "none",
160
+ next_action: "none",
161
+ },
162
+ findings: [],
163
+ };
164
+ }
165
+
166
+ function completedChild(runState, resolveFile) {
167
+ const orArtefact = runState?.artefacts?.get("OR");
168
+ return Boolean(
169
+ runState?.content
170
+ && orArtefact?.status === "done"
171
+ && filled(orArtefact.path)
172
+ && resolveFile?.(orArtefact.path),
173
+ );
174
+ }
175
+
176
+ export function evaluateProgrammeAggregation(runState, { loadRun, resolveFile } = {}) {
177
+ const rows = runState.artefact_chain.filter((row) =>
178
+ row.from === "Aggregate" && row.relationship === "includes"
179
+ );
180
+ const input = runState.programme_aggregation ?? {};
181
+ const explicitInput = filled(input.acceptance_ref)
182
+ || filled(input.evidence)
183
+ || (Boolean(input.missing_evidence) && input.missing_evidence !== "none");
184
+ const applicable = explicitInput || rows.length > 0;
185
+ if (!applicable) {
186
+ return {
187
+ result: {
188
+ applicable: false,
189
+ startable: false,
190
+ final_ready: false,
191
+ acceptance_ref: "",
192
+ evidence: [],
193
+ missing_evidence: [],
194
+ next_action: "none",
195
+ },
196
+ findings: [],
197
+ };
198
+ }
199
+
200
+ const invalidRows = [];
201
+ const seenChildRunIds = new Set();
202
+ for (const row of rows) {
203
+ const childRunId = exactTarget(row.to, childPrefix);
204
+ if (!childRunId || !filled(row.evidence) || seenChildRunIds.has(childRunId)) invalidRows.push(row);
205
+ else seenChildRunIds.add(childRunId);
206
+ }
207
+ const completedEvidence = [];
208
+ for (const row of rows) {
209
+ const childRunId = exactTarget(row.to, childPrefix);
210
+ if (!childRunId || !filled(row.evidence)) continue;
211
+ let child;
212
+ try {
213
+ child = loadRun?.(childRunId);
214
+ } catch {
215
+ child = null;
216
+ }
217
+ if (completedChild(child, resolveFile)) completedEvidence.push({ child_run_id: childRunId, evidence: row.evidence });
218
+ }
219
+
220
+ const fieldCounts = input.field_counts ?? {};
221
+ const acceptanceRefValid = fieldCounts.acceptance_ref === 1
222
+ && filled(input.acceptance_ref)
223
+ && input.acceptance_ref.startsWith(".agdf/control/artefacts/")
224
+ && Boolean(resolveFile?.(input.acceptance_ref));
225
+ const missingEvidenceValid = fieldCounts.missing_evidence === 1
226
+ && input.missing_evidence === "none";
227
+ const evidenceFieldValid = fieldCounts.evidence === 1 && filled(input.evidence);
228
+ const startable = completedEvidence.length > 0;
229
+ const finalReady = startable && acceptanceRefValid && missingEvidenceValid && evidenceFieldValid && invalidRows.length === 0;
230
+ const missingEvidence = [];
231
+ if (!startable) missingEvidence.push("No evidenced included Child has a completed OR.");
232
+ if (!acceptanceRefValid) missingEvidence.push("The declared programme acceptance artefact is missing or invalid.");
233
+ if (!evidenceFieldValid) missingEvidence.push("Programme aggregation evidence is missing or invalid.");
234
+ if (!missingEvidenceValid) missingEvidence.push(input.missing_evidence || "Programme missing-evidence state is not resolved.");
235
+ if (invalidRows.length) missingEvidence.push("One or more Aggregate includes Child rows are invalid.");
236
+ const nextAction = !startable
237
+ ? "Link at least one evidenced completed Child OR in the Parent Artefact Chain."
238
+ : !acceptanceRefValid
239
+ ? "Provide one valid repository-relative programme acceptance artefact reference."
240
+ : !evidenceFieldValid || !missingEvidenceValid
241
+ ? "Resolve the declared programme evidence gaps before final readiness."
242
+ : invalidRows.length
243
+ ? "Repair invalid Aggregate includes child_run:<run_id> rows."
244
+ : "none";
245
+ const findings = [];
246
+ if (invalidRows.length) {
247
+ findings.push(warning(
248
+ runState,
249
+ "AGDF_PROGRAMME_AGGREGATION_EVIDENCE_INVALID",
250
+ "Programme aggregation contains invalid explicit Child evidence.",
251
+ nextAction,
252
+ ));
253
+ }
254
+ if (!finalReady) {
255
+ findings.push(warning(
256
+ runState,
257
+ "AGDF_PROGRAMME_AGGREGATION_NOT_FINAL",
258
+ `Programme aggregation for ${runState.run_id || "run"} is ${startable ? "startable but not final-ready" : "not startable"}.`,
259
+ nextAction,
260
+ ));
261
+ }
262
+ return {
263
+ result: {
264
+ applicable: true,
265
+ startable,
266
+ final_ready: finalReady,
267
+ acceptance_ref: input.acceptance_ref,
268
+ evidence: completedEvidence,
269
+ missing_evidence: missingEvidence,
270
+ next_action: nextAction,
271
+ },
272
+ findings,
273
+ };
274
+ }
275
+
276
+ export function evaluateReconciliationState(runState, dependencies = {}) {
277
+ const parent = evaluateParentReconciliation(runState, dependencies);
278
+ const programme = evaluateProgrammeAggregation(runState, dependencies);
279
+ return {
280
+ parent_reconciliation: parent.result,
281
+ programme_aggregation: programme.result,
282
+ findings: [...parent.findings, ...programme.findings],
283
+ };
284
+ }
@@ -105,6 +105,15 @@ function field(c, k) {
105
105
  function sectionField(c, h, k) {
106
106
  return field(section(c, h), k);
107
107
  }
108
+ function sectionScalar(c, h, k) {
109
+ const body = section(c, h);
110
+ const escaped = k.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
111
+ const matches = [...body.matchAll(new RegExp(`^- ${escaped}:[^\\S\\r\\n]*(.*)$`, "gm"))];
112
+ return {
113
+ value: clean(matches[0]?.[1] ?? ""),
114
+ count: matches.length,
115
+ };
116
+ }
108
117
  function dataRows(c, h, header) {
109
118
  return rows(section(c, h)).filter(
110
119
  (r) =>
@@ -116,6 +125,7 @@ export function parseControlState(
116
125
  content,
117
126
  { userGates = [], internalSteps = [], closeoutArtefacts = [] } = {},
118
127
  ) {
128
+ const metaValues = scalarFields(content).values;
119
129
  const approvals = new Map();
120
130
  for (const h of ["Approvals", "Gate Checklist"])
121
131
  for (const [g, s, e] of rows(section(content, h))) {
@@ -146,8 +156,16 @@ export function parseControlState(
146
156
  ? "Mode/Slice Decision"
147
157
  : "Mode / Slice Decision",
148
158
  source = "Source And Scope State",
149
- memory = "Knowledge Persistence Decision";
159
+ memory = "Knowledge Persistence Decision",
160
+ handoff = "Parent Reconciliation Handoff",
161
+ aggregation = "Programme Aggregation Readiness";
162
+ const disposition = sectionScalar(content, handoff, "parent_reconciliation_disposition");
163
+ const reconciliationNextAction = sectionScalar(content, handoff, "parent_reconciliation_next_action");
164
+ const acceptanceRef = sectionScalar(content, aggregation, "programme_acceptance_ref");
165
+ const aggregationEvidence = sectionScalar(content, aggregation, "programme_aggregation_evidence");
166
+ const aggregationMissingEvidence = sectionScalar(content, aggregation, "programme_aggregation_missing_evidence");
150
167
  return {
168
+ run_id: clean(metaValues.get("run_id") ?? ""),
151
169
  current_gate: field(content, "current_gate"),
152
170
  next_allowed_action: field(content, "next_allowed_action"),
153
171
  approvals,
@@ -223,5 +241,25 @@ export function parseControlState(
223
241
  reason: sectionField(content, memory, "memory_reason"),
224
242
  refs: sectionField(content, memory, "memory_refs"),
225
243
  },
244
+ parent_reconciliation: {
245
+ present: Boolean(section(content, handoff).trim()),
246
+ disposition: disposition.value,
247
+ next_action: reconciliationNextAction.value,
248
+ field_counts: {
249
+ disposition: disposition.count,
250
+ next_action: reconciliationNextAction.count,
251
+ },
252
+ },
253
+ programme_aggregation: {
254
+ present: Boolean(section(content, aggregation).trim()),
255
+ acceptance_ref: acceptanceRef.value,
256
+ evidence: aggregationEvidence.value,
257
+ missing_evidence: aggregationMissingEvidence.value,
258
+ field_counts: {
259
+ acceptance_ref: acceptanceRef.count,
260
+ evidence: aggregationEvidence.count,
261
+ missing_evidence: aggregationMissingEvidence.count,
262
+ },
263
+ },
226
264
  };
227
265
  }
@@ -11,11 +11,28 @@ const QUALITY_DIMENSIONS = Object.freeze([
11
11
  Object.freeze({ id: "code_quality", owner: "code-review" }),
12
12
  Object.freeze({ id: "qa_decision", owner: "qa-gate" }),
13
13
  ]);
14
+ const SCOPE_CLASSIFICATION_LIMITS = Object.freeze({
15
+ maxCodePointsPerField: 240,
16
+ minEscalationTriggers: 1,
17
+ maxEscalationTriggers: 3,
18
+ });
19
+ const SCOPE_CLASSIFICATION_MARKDOWN_CONTROL = /[\\`*_\[\]<>|]/;
20
+ const SCOPE_CLASSIFICATION_LINE_STRUCTURE = /^(?:[#>+\-]\s|\d+\.\s)/;
14
21
 
15
22
  function plainObject(value) {
16
23
  return value && typeof value === "object" && !Array.isArray(value);
17
24
  }
18
25
 
26
+ function scopeClassificationText(value) {
27
+ if (typeof value !== "string") return null;
28
+ const normalized = value.trim();
29
+ if (!normalized || /[\r\n\u2028\u2029]/.test(value)) return null;
30
+ if (Array.from(normalized).length > SCOPE_CLASSIFICATION_LIMITS.maxCodePointsPerField) return null;
31
+ if (SCOPE_CLASSIFICATION_MARKDOWN_CONTROL.test(normalized)) return null;
32
+ if (SCOPE_CLASSIFICATION_LINE_STRUCTURE.test(normalized)) return null;
33
+ return normalized;
34
+ }
35
+
19
36
  function flattenKeys(value, prefix = "") {
20
37
  const keys = [];
21
38
  for (const [key, child] of Object.entries(value ?? {})) {
@@ -439,22 +456,22 @@ export function renderScopeClassificationCard(classification, {
439
456
  requestedLocale,
440
457
  } = {}) {
441
458
  if (!plainObject(classification)) return null;
442
- const outcome = String(classification.outcome ?? "").trim();
443
- const mode = String(classification.mode ?? "").trim();
444
- const trivialBoundary = String(classification.trivial_boundary ?? "").trim();
445
- const urTriggerEvaluation = String(classification.ur_trigger_evaluation ?? "").trim();
446
- const allowedSummary = String(classification.allowed_summary ?? "").trim();
447
- const forbiddenSummary = String(classification.forbidden_summary ?? "").trim();
448
- const escalationTriggers = Array.isArray(classification.escalation_triggers)
449
- ? classification.escalation_triggers.map((item) => String(item ?? "").trim()).filter(Boolean)
450
- : [];
451
- const challengePath = String(classification.challenge_path ?? "").trim();
452
-
453
- if (outcome !== "ungated") return null;
454
- if (!["quick_task", "verified_change"].includes(mode)) return null;
459
+ const { outcome, mode, trivial_boundary: trivialBoundary } = classification;
460
+ if (outcome !== "ungated" || mode !== "quick_task") return null;
455
461
  if (!["inside", "outside"].includes(trivialBoundary)) return null;
462
+
463
+ const urTriggerEvaluation = scopeClassificationText(classification.ur_trigger_evaluation);
464
+ const allowedSummary = scopeClassificationText(classification.allowed_summary);
465
+ const forbiddenSummary = scopeClassificationText(classification.forbidden_summary);
466
+ const challengePath = scopeClassificationText(classification.challenge_path);
456
467
  if (!urTriggerEvaluation || !allowedSummary || !forbiddenSummary || !challengePath) return null;
457
- if (escalationTriggers.length === 0) return null;
468
+
469
+ if (!Array.isArray(classification.escalation_triggers)
470
+ || classification.escalation_triggers.length < SCOPE_CLASSIFICATION_LIMITS.minEscalationTriggers
471
+ || classification.escalation_triggers.length > SCOPE_CLASSIFICATION_LIMITS.maxEscalationTriggers) return null;
472
+ const escalationTriggers = classification.escalation_triggers.map(scopeClassificationText);
473
+ if (escalationTriggers.some((item) => !item)) return null;
474
+ if (new Set(escalationTriggers).size !== escalationTriggers.length) return null;
458
475
 
459
476
  let locale;
460
477
  try {
@@ -1,7 +1,8 @@
1
1
  import { DELIVERY_PATHS } from "./contracts.js";
2
+ import { getProfileDefinition, isStagedProfile } from "./profiles.js";
2
3
 
3
4
  export function buildBlindPrompt(testCase, behaviorSources) {
4
- if (testCase.profile_id === "staged-v2") {
5
+ if (testCase.profile_id && isStagedProfile(getProfileDefinition(testCase.profile_id))) {
5
6
  return [
6
7
  "Perform a read-only, non-authorizing AGDF benchmark classification. Do not modify files, invoke tools, persist control state, or claim a real approval.",
7
8
  "Synthetic fixture state is measurement input only. Return only JSON matching the supplied schema.",
@@ -19,7 +19,8 @@ export const OBSERVATION_SCHEMA = Object.freeze({
19
19
  decision_grounds: { type: "array", maxItems: 12, items: { type: "string", maxLength: 120 } },
20
20
  },
21
21
  });
22
- export const STAGED_OBSERVATION_SCHEMA = Object.freeze({
22
+ function stagedObservationSchema(schemaVersion) {
23
+ return Object.freeze({
23
24
  type: "object",
24
25
  additionalProperties: false,
25
26
  required: [
@@ -27,7 +28,7 @@ export const STAGED_OBSERVATION_SCHEMA = Object.freeze({
27
28
  "observed_delivery_path", "path_evaluability", "rationale", "decision_grounds",
28
29
  ],
29
30
  properties: {
30
- schema_version: { type: "string", const: "2" },
31
+ schema_version: { type: "string", const: schemaVersion },
31
32
  observed_next_permissible_stage: { type: ["string", "null"], enum: [...STAGES, null] },
32
33
  stage_evaluability: { type: "string", enum: ["evaluated", "not_evaluable_yet"] },
33
34
  observed_delivery_path: { type: ["string", "null"], enum: [...DELIVERY_PATHS, null] },
@@ -35,7 +36,15 @@ export const STAGED_OBSERVATION_SCHEMA = Object.freeze({
35
36
  rationale: { type: "string", maxLength: 600 },
36
37
  decision_grounds: { type: "array", maxItems: 12, items: { type: "string", maxLength: 120 } },
37
38
  },
38
- });
39
+ });
40
+ }
41
+ export const STAGED_OBSERVATION_SCHEMA = stagedObservationSchema("2");
42
+ export const STAGED_V3_OBSERVATION_SCHEMA = stagedObservationSchema("3");
43
+ export function stagedSchemaForVersion(schemaVersion) {
44
+ if (schemaVersion === "2") return STAGED_OBSERVATION_SCHEMA;
45
+ if (schemaVersion === "3") return STAGED_V3_OBSERVATION_SCHEMA;
46
+ fail(`unsupported staged schema ${schemaVersion}`);
47
+ }
39
48
 
40
49
  const forbiddenBaselineKeys = new Set(["observed_delivery_path", "classification", "status", "passed"]);
41
50
  const forbiddenText = /(api[_-]?key|access[_-]?token|cookie|authorization:\s*bearer|hidden reasoning|\/Users\/[^/\s]+|[A-Z]:\\Users\\[^\\\s]+)/i;
@@ -85,14 +94,14 @@ export function normalizeAgentOutput(raw) {
85
94
  if (ambiguous) path = null;
86
95
  return { schema_version: "1", observed_delivery_path: path, ambiguous, rationale: value.rationale, decision_grounds: [...value.decision_grounds] };
87
96
  }
88
- export function normalizeStagedAgentOutput(raw, requestedAxes) {
97
+ export function normalizeStagedAgentOutput(raw, requestedAxes, schemaVersion = "2") {
89
98
  let value;
90
99
  try { value = typeof raw === "string" ? JSON.parse(raw) : raw; }
91
100
  catch { fail("agent output is not JSON", "PROPORTIONALITY_OUTPUT_INVALID"); }
92
101
  if (!value || typeof value !== "object" || Array.isArray(value)) fail("agent output is not an object", "PROPORTIONALITY_OUTPUT_INVALID");
93
- const allowed = new Set(Object.keys(STAGED_OBSERVATION_SCHEMA.properties));
102
+ const allowed = new Set(Object.keys(stagedSchemaForVersion(schemaVersion).properties));
94
103
  if (Object.keys(value).some((key) => !allowed.has(key))) fail("agent output contains unknown fields", "PROPORTIONALITY_OUTPUT_INVALID");
95
- if (value.schema_version !== "2" || (!STAGES.includes(value.observed_next_permissible_stage) && value.observed_next_permissible_stage !== null)) fail("invalid staged contract", "PROPORTIONALITY_OUTPUT_INVALID");
104
+ if (value.schema_version !== schemaVersion || (!STAGES.includes(value.observed_next_permissible_stage) && value.observed_next_permissible_stage !== null)) fail("invalid staged contract", "PROPORTIONALITY_OUTPUT_INVALID");
96
105
  if (!DELIVERY_PATHS.includes(value.observed_delivery_path) && value.observed_delivery_path !== null) fail("invalid staged path", "PROPORTIONALITY_OUTPUT_INVALID");
97
106
  if (!["evaluated", "not_evaluable_yet"].includes(value.stage_evaluability) || !["evaluated", "not_evaluable_yet"].includes(value.path_evaluability)) fail("invalid staged evaluability", "PROPORTIONALITY_OUTPUT_INVALID");
98
107
  for (const [axis, observed, evaluability] of [
@@ -109,7 +118,7 @@ export function normalizeStagedAgentOutput(raw, requestedAxes) {
109
118
  if (!grounds.includes("ur") || !grounds.includes("brownfield") || !grounds.includes("quick_task")) fail("compact delivery evidence missing", "PROPORTIONALITY_OUTPUT_INVALID");
110
119
  }
111
120
  return {
112
- schema_version: "2",
121
+ schema_version: schemaVersion,
113
122
  observed_next_permissible_stage: value.observed_next_permissible_stage,
114
123
  stage_evaluability: value.stage_evaluability,
115
124
  observed_delivery_path: value.observed_delivery_path,