agentera 3.0.0-dev.51 → 3.0.0-dev.69
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 +5 -5
- package/bundle/CHANGELOG.md +21 -11
- package/bundle/UPGRADE.md +23 -7
- package/bundle/references/adapters/package-publication.json +68 -17
- package/bundle/references/adapters/package-registry.yaml +145 -145
- package/bundle/references/analysis/verification-policy.yaml +93 -18
- package/bundle/references/artifacts/state-storage-authority.yaml +50 -11
- package/bundle/skills/agentera/SKILL.md +7 -5
- package/bundle/skills/agentera/capabilities/build/schemas/artifacts.yaml +7 -6
- package/bundle/skills/agentera/capabilities/build/schemas/exit.yaml +6 -5
- package/bundle/skills/agentera/capabilities/build/schemas/validation.yaml +18 -10
- package/bundle/skills/agentera/schemas/artifacts/plan.yaml +3 -0
- package/bundle/skills/agentera/schemas/artifacts/progress.yaml +8 -6
- package/dist/capabilities/build/instructions.js +8 -5
- package/dist/cli/capabilityContext/build.js +12 -3
- package/dist/cli/capabilityContext/planState.js +6 -2
- package/dist/cli/capabilityContext/startupAggregation.js +1 -1
- package/dist/cli/commands/doctor.js +1 -1
- package/dist/cli/commands/prime/briefOrientation.js +0 -1
- package/dist/cli/commands/prime/collectOrientationState.js +13 -6
- package/dist/cli/commands/prime/orientationOutput.js +27 -5
- package/dist/cli/commands/state/write.js +4 -1
- package/dist/cli/dispatch/index.js +1 -1
- package/dist/cli/help.js +1 -0
- package/dist/cli/migrationRequired.js +40 -5
- package/dist/registries/activationTuples.js +3 -2
- package/dist/registries/packagePublication.js +51 -7
- package/dist/state/entityMigrationPreview.js +3 -9
- package/dist/state/entityPublicationContext.js +56 -3
- package/dist/state/planEntities.js +49 -4
- package/dist/state/progressWritePolicy.js +33 -0
- package/dist/state/stateMode.js +138 -1
- package/dist/state/todoDocsEntities.js +62 -7
- package/dist/state/todoReconciliationActivation.js +46 -4
- package/dist/state/todoReconciliationInspection.js +5 -3
- package/dist/state/todoReconciliationRepair.js +149 -1
- package/dist/state/todoReconciliationTransaction.js +20 -7
- package/dist/state/write/explain.js +17 -3
- package/dist/state/write/input.js +25 -1
- package/dist/state/write/runtimeOperations.js +4 -2
- package/dist/state/write/transaction.js +6 -1
- package/dist/validate/activationArtifactEvidence.js +49 -41
- package/dist/validate/activationArtifactEvidenceTypes.js +12 -0
- package/dist/validate/activationEvidenceManifest.js +26 -10
- package/package.json +11 -6
|
@@ -12,7 +12,7 @@ import { v1MigrationSummary } from "./v1Migration.js";
|
|
|
12
12
|
import { diagnoseCanonicalSkill } from "../../../setup/sharedSkill.js";
|
|
13
13
|
import { collectEntityOrientation } from "./collectEntityOrientation.js";
|
|
14
14
|
import { acquireProfile } from "../../profileAcquisition.js";
|
|
15
|
-
import { fullEntityUpgradeCommand } from "../../../upgrade/upgradeCommands.js";
|
|
15
|
+
import { fullEntityUpgradeCommand, fullEntityUpgradePreviewCommand } from "../../../upgrade/upgradeCommands.js";
|
|
16
16
|
import { classifyEntityCutoverProject } from "../../../state/entityMigrationPreview.js";
|
|
17
17
|
import { preCutoverCommand } from "../../preCutoverCommand.js";
|
|
18
18
|
const EMPTY_SCHEMAS = Object.freeze({});
|
|
@@ -22,7 +22,14 @@ function stateCutover(project, sourceRoot) {
|
|
|
22
22
|
if (projectState === "v3") {
|
|
23
23
|
return { status: "complete", project_state: "v3", recovery_command: null };
|
|
24
24
|
}
|
|
25
|
-
|
|
25
|
+
if (projectState === "fresh_uninitialized") {
|
|
26
|
+
return { status: "fresh_uninitialized", project_state: projectState, recovery_command: null };
|
|
27
|
+
}
|
|
28
|
+
return {
|
|
29
|
+
status: "required",
|
|
30
|
+
project_state: projectState,
|
|
31
|
+
recovery_command: projectState === "legacy" ? fullEntityUpgradeCommand(project) : fullEntityUpgradePreviewCommand(project),
|
|
32
|
+
};
|
|
26
33
|
}
|
|
27
34
|
catch {
|
|
28
35
|
return { status: "invalid_lifecycle", project_state: "invalid_lifecycle", recovery_command: preCutoverCommand("check validate state --format json") };
|
|
@@ -99,13 +106,13 @@ export function collectOrientationState(opts) {
|
|
|
99
106
|
const readiness = selectStatusReadiness(plan, health, objective, todoItems, decision, savedContext, entity.todoReadiness);
|
|
100
107
|
const reconciliationReadiness = entity.todoReconciliation?.status === "action_required"
|
|
101
108
|
? withRecommended(readiness, {
|
|
102
|
-
object: entity.todoReconciliation.state === "unsafe_inactive" ? "
|
|
103
|
-
capability:
|
|
109
|
+
object: entity.todoReconciliation.state === "unsafe_inactive" ? "Correct unsafe TODO ownership" : entity.todoReconciliation.state === "inactive" ? "Activate TODO reconciliation" : "Repair TODO reconciliation",
|
|
110
|
+
capability: "build",
|
|
104
111
|
reason: entity.todoReconciliation.recovery_command,
|
|
105
|
-
phase: entity.todoReconciliation.state === "unsafe_inactive" ? "
|
|
112
|
+
phase: entity.todoReconciliation.state === "unsafe_inactive" ? "build" : entity.todoReconciliation.state === "invalid_lifecycle" ? "audit" : "build",
|
|
106
113
|
})
|
|
107
114
|
: readiness;
|
|
108
|
-
const nextAction = cutover.status
|
|
115
|
+
const nextAction = cutover.status === "required" || cutover.status === "invalid_lifecycle"
|
|
109
116
|
? withRecommended(readiness, {
|
|
110
117
|
object: "Complete Agentera entity-state cutover",
|
|
111
118
|
capability: "status",
|
|
@@ -32,8 +32,7 @@ function nextActionEntry(action) {
|
|
|
32
32
|
* and `alternatives` carries the cascade branches the early-return model
|
|
33
33
|
* would have skipped, each with the same `{object, capability, reason, phase}`
|
|
34
34
|
* shape. */
|
|
35
|
-
function nextActionPayload(
|
|
36
|
-
const { recommended, alternatives } = state.next_action;
|
|
35
|
+
function nextActionPayload({ recommended, alternatives }) {
|
|
37
36
|
return {
|
|
38
37
|
...nextActionEntry(recommended),
|
|
39
38
|
alternatives: alternatives.map(nextActionEntry),
|
|
@@ -163,7 +162,7 @@ export function buildOrientationJsonPayload(state, command, options = {}) {
|
|
|
163
162
|
attention: projectPublicOrientationAttention(state).map((item) => truncateCodePoints(item, 200, "…")),
|
|
164
163
|
decision_attention: state.decision_attention,
|
|
165
164
|
history: state.history,
|
|
166
|
-
next_action: nextActionPayload(state),
|
|
165
|
+
next_action: nextActionPayload(state.next_action),
|
|
167
166
|
startup,
|
|
168
167
|
orchestration_context: bespoke.orchestration_context,
|
|
169
168
|
closeout_context: bespoke.closeout_context,
|
|
@@ -192,6 +191,29 @@ export function buildStatusContextState(state, _command = "prime", _options = {}
|
|
|
192
191
|
const firstPending = plan.first_pending && typeof plan.first_pending === "object" && !Array.isArray(plan.first_pending)
|
|
193
192
|
? plan.first_pending
|
|
194
193
|
: null;
|
|
194
|
+
const risks = todoReconciliation?.risks;
|
|
195
|
+
const unsafeDetailOmitted = todoReconciliation?.state === "unsafe_inactive"
|
|
196
|
+
&& Boolean(risks && typeof risks === "object" && !Array.isArray(risks) && Number(risks.omitted_count) > 0);
|
|
197
|
+
const recovery = unsafeDetailOmitted && typeof todoReconciliation.recovery_command === "string"
|
|
198
|
+
? todoReconciliation.recovery_command
|
|
199
|
+
: null;
|
|
200
|
+
const todoAttention = unsafeDetailOmitted
|
|
201
|
+
? new Set(state.todo_items.map((item) => `${String(item.severity)}: TODO: ${String(item.text)}`))
|
|
202
|
+
: new Set();
|
|
203
|
+
const attention = projectPublicOrientationAttention(state)
|
|
204
|
+
.flatMap((item) => item === `action-required: TODO reconciliation is unsafe inactive; ${recovery}`
|
|
205
|
+
? ["action-required: TODO reconciliation is unsafe inactive; see capability_context.startup.todo_reconciliation"]
|
|
206
|
+
: todoAttention.has(item) ? [] : [item])
|
|
207
|
+
.map((item) => truncateCodePoints(item, 200, "…"));
|
|
208
|
+
const redactRecovery = (action) => recovery !== null && action.reason === recovery
|
|
209
|
+
? { ...action, reason: "See capability_context.startup.todo_reconciliation for recovery." }
|
|
210
|
+
: action;
|
|
211
|
+
const nextAction = {
|
|
212
|
+
recommended: redactRecovery(state.next_action.recommended),
|
|
213
|
+
alternatives: state.next_action.alternatives
|
|
214
|
+
.filter((action) => !unsafeDetailOmitted || action.artifact !== "todo")
|
|
215
|
+
.map(redactRecovery),
|
|
216
|
+
};
|
|
195
217
|
return {
|
|
196
218
|
outcome: startup.outcome,
|
|
197
219
|
mode: state.mode,
|
|
@@ -231,8 +253,8 @@ export function buildStatusContextState(state, _command = "prime", _options = {}
|
|
|
231
253
|
progress: { exists: state.progress.exists, status: state.progress.status ?? null, latest: state.progress.latest ?? null },
|
|
232
254
|
objective: { exists: state.objective.exists, active: state.objective.active ?? false, title: state.objective.title ?? null },
|
|
233
255
|
state_presence: state.state_presence,
|
|
234
|
-
attention
|
|
235
|
-
next_action: nextActionPayload(
|
|
256
|
+
attention,
|
|
257
|
+
next_action: nextActionPayload(nextAction),
|
|
236
258
|
};
|
|
237
259
|
}
|
|
238
260
|
export function emitPrime(command, payload, format, fieldsArg, out, err, options = {}) {
|
|
@@ -280,7 +280,10 @@ function parseWrite(artifactRaw, argv) {
|
|
|
280
280
|
example: exampleFor(artifact, verb),
|
|
281
281
|
});
|
|
282
282
|
const selectorFields = new Set(spec.fields.filter((field) => spec.selectors.includes(field.flag)).map((field) => field.field));
|
|
283
|
-
|
|
283
|
+
const correctionApplyFields = artifact === "todo" && verb === "correct-owners"
|
|
284
|
+
? new Set(["effect_sha256", "confirmed"])
|
|
285
|
+
: new Set();
|
|
286
|
+
if (spec.inputRoot && Object.keys(values).some((field) => !selectorFields.has(field) && !correctionApplyFields.has(field)))
|
|
284
287
|
invalid({
|
|
285
288
|
class: "mutually_exclusive",
|
|
286
289
|
message: `--input cannot be combined with field flags for ${artifact} ${verb}`,
|
|
@@ -63,7 +63,7 @@ export function main(argv, io = {}) {
|
|
|
63
63
|
// must report its source-checkout boundary before project migration checks.
|
|
64
64
|
const sourceOnlyReferenceValidation = args[0] === "check" && args[1] === "validate" && ["retained-references", "activation-conjunction"].includes(args[2] ?? "");
|
|
65
65
|
if (!sourceOnlyReferenceValidation && requiresCompletedEntityCutover(args)) {
|
|
66
|
-
const failure = enforceCompletedEntityCutover(migrationProject(args), requestedMigrationFailureFormat(args), io);
|
|
66
|
+
const failure = enforceCompletedEntityCutover(migrationProject(args), requestedMigrationFailureFormat(args), io, args);
|
|
67
67
|
if (failure !== null)
|
|
68
68
|
return failure;
|
|
69
69
|
}
|
package/dist/cli/help.js
CHANGED
|
@@ -204,6 +204,7 @@ export function printStateHelp(sub) {
|
|
|
204
204
|
return [
|
|
205
205
|
...recordFamilyReadSection("todo"),
|
|
206
206
|
" agentera state todo activate|repair --dry-run|--effect-sha256 SHA256 --yes --format json",
|
|
207
|
+
" agentera state todo correct-owners --input OWNER_MAPPING.yaml --dry-run|--effect-sha256 SHA256 --yes --format json",
|
|
207
208
|
" agentera state todo create --input TODO.yaml --format json",
|
|
208
209
|
" agentera state todo update --id ID --input TODO-PATCH.yaml --format json",
|
|
209
210
|
" agentera state todo set-severity --id ID --severity LEVEL --reason TEXT --date YYYY-MM-DD --format json",
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { emitStructured } from "./structured.js";
|
|
4
|
-
import { detectStateMode } from "../state/stateMode.js";
|
|
5
|
-
import { fullEntityUpgradeCommand } from "../upgrade/upgradeCommands.js";
|
|
4
|
+
import { classifyProjectState, detectStateMode } from "../state/stateMode.js";
|
|
5
|
+
import { commandText, fullEntityUpgradeCommand, fullEntityUpgradePreviewCommand } from "../upgrade/upgradeCommands.js";
|
|
6
6
|
import { preCutoverCommand } from "./preCutoverCommand.js";
|
|
7
7
|
function value(argv, flag) {
|
|
8
8
|
const equals = argv.find((arg) => typeof arg === "string" && arg.startsWith(`${flag}=`));
|
|
@@ -46,8 +46,19 @@ export function migrationProject(argv, fallback = process.cwd()) {
|
|
|
46
46
|
candidate = parent;
|
|
47
47
|
}
|
|
48
48
|
}
|
|
49
|
+
function permitsFreshPlanInitialization(argv) {
|
|
50
|
+
const [command, subcommand, verb] = argv;
|
|
51
|
+
return command === "state" && subcommand === "plan" && verb === "create"
|
|
52
|
+
|| command === "prime" && value(argv, "--context") === "plan";
|
|
53
|
+
}
|
|
54
|
+
function freshPlanCreateCommand(project) {
|
|
55
|
+
return commandText([
|
|
56
|
+
"npx", "-y", "agentera@next", "state", "plan", "create", "--project", project,
|
|
57
|
+
"--input", "PLAN.yaml", "--format", "json",
|
|
58
|
+
]);
|
|
59
|
+
}
|
|
49
60
|
/** Read-only cutover gate. It only inspects the durable marker and emits a failure. */
|
|
50
|
-
export function enforceCompletedEntityCutover(projectRoot, format, io = {}) {
|
|
61
|
+
export function enforceCompletedEntityCutover(projectRoot, format, io = {}, argv = []) {
|
|
51
62
|
const project = path.resolve(projectRoot);
|
|
52
63
|
let mode;
|
|
53
64
|
try {
|
|
@@ -68,13 +79,37 @@ export function enforceCompletedEntityCutover(projectRoot, format, io = {}) {
|
|
|
68
79
|
}
|
|
69
80
|
if (mode === "entities")
|
|
70
81
|
return null;
|
|
71
|
-
const
|
|
82
|
+
const classification = classifyProjectState(project);
|
|
83
|
+
if (classification.state === "fresh_uninitialized") {
|
|
84
|
+
if (permitsFreshPlanInitialization(argv))
|
|
85
|
+
return null;
|
|
86
|
+
const recovery = freshPlanCreateCommand(project);
|
|
87
|
+
const envelope = {
|
|
88
|
+
schemaVersion: "agentera.stateFailure.v1",
|
|
89
|
+
status: "fail",
|
|
90
|
+
error: {
|
|
91
|
+
class: "fresh_initialization_required",
|
|
92
|
+
message: "Fresh project state is initialized only by state plan create; no other state writer can create entity authority.",
|
|
93
|
+
project,
|
|
94
|
+
recovery,
|
|
95
|
+
},
|
|
96
|
+
};
|
|
97
|
+
if (format === "json" || format === "yaml")
|
|
98
|
+
emitStructured(envelope, format, io.out ?? ((text) => process.stdout.write(text)));
|
|
99
|
+
else
|
|
100
|
+
(io.err ?? ((text) => process.stderr.write(text)))(`Error: ${envelope.error.message}\nRecovery: ${recovery}\n`);
|
|
101
|
+
return 1;
|
|
102
|
+
}
|
|
103
|
+
const legacy = classification.state === "legacy";
|
|
104
|
+
const recovery = legacy ? fullEntityUpgradeCommand(project) : fullEntityUpgradePreviewCommand(project);
|
|
72
105
|
const envelope = {
|
|
73
106
|
schemaVersion: "agentera.stateFailure.v1",
|
|
74
107
|
status: "fail",
|
|
75
108
|
error: {
|
|
76
109
|
class: "migration_required",
|
|
77
|
-
message:
|
|
110
|
+
message: legacy
|
|
111
|
+
? "This command requires the completed entity-state cutover; legacy aggregates remain migration input only."
|
|
112
|
+
: `This command cannot adopt marker-absent ${classification.state} Agentera state; inspect it with the read-only recovery command before any mutation.`,
|
|
78
113
|
project,
|
|
79
114
|
recovery,
|
|
80
115
|
},
|
|
@@ -76,6 +76,7 @@ const rawTuples = JSON.parse(decodedCatalog.slice(decodedCatalog.indexOf("[", 2)
|
|
|
76
76
|
const addedTuples = [
|
|
77
77
|
{ class: "state", surface_id: "write:plan.replace", owner_path: "packages/cli/src/state/write/runtimeOperations.ts", owner_symbol_or_selector: "runtimeOperationSpecs", owner_selector: "plan.replace", semantic_selector_if_any: null, canonical_correction: "node packages/cli/dist/bin/agentera.js check validate state --format json" },
|
|
78
78
|
{ class: "state", surface_id: "write:todo.activate", owner_path: "packages/cli/src/state/write/runtimeOperations.ts", owner_symbol_or_selector: "runtimeOperationSpecs", owner_selector: "todo.activate", semantic_selector_if_any: null, canonical_correction: "node packages/cli/dist/bin/agentera.js check validate state --format json" },
|
|
79
|
+
{ class: "state", surface_id: "write:todo.correct-owners", owner_path: "packages/cli/src/state/write/runtimeOperations.ts", owner_symbol_or_selector: "runtimeOperationSpecs", owner_selector: "todo.correct-owners", semantic_selector_if_any: null, canonical_correction: "node packages/cli/dist/bin/agentera.js check validate state --format json" },
|
|
79
80
|
{ class: "state", surface_id: "write:todo.repair", owner_path: "packages/cli/src/state/write/runtimeOperations.ts", owner_symbol_or_selector: "runtimeOperationSpecs", owner_selector: "todo.repair", semantic_selector_if_any: null, canonical_correction: "node packages/cli/dist/bin/agentera.js check validate state --format json" },
|
|
80
81
|
{ class: "package", surface_id: "emitted:packages/cli/src/cli/commands/doctor.ts", owner_path: "packages/cli/src/registries/packageRegistry.ts", owner_symbol_or_selector: "loadRegistry", owner_selector: "packages/cli/src/cli/commands/doctor.ts", semantic_selector_if_any: JSON.stringify({ path: "packages/cli/src/cli/commands/doctor.ts", selector: null, format: null, classification: null, reason: "Doctor project-state signals publish bounded reconciliation preview and apply guidance." }), canonical_correction: "pnpm -C packages/cli run verify:package" },
|
|
81
82
|
{ class: "package", surface_id: "emitted:packages/cli/src/state/todoReconciliationInspection.ts", owner_path: "packages/cli/src/registries/packageRegistry.ts", owner_symbol_or_selector: "loadRegistry", owner_selector: "packages/cli/src/state/todoReconciliationInspection.ts", semantic_selector_if_any: JSON.stringify({ path: "packages/cli/src/state/todoReconciliationInspection.ts", selector: null, format: null, classification: null, reason: "TODO reconciliation inspection publishes bounded preview and effect-bound apply guidance." }), canonical_correction: "pnpm -C packages/cli run verify:package" },
|
|
@@ -95,11 +96,11 @@ export const ACTIVATION_TUPLE_AUTHORITY = Object.freeze({
|
|
|
95
96
|
capability: { count: 12, sha256: "892e6e5e2a57b41064bc44fa2946453225f1b1195aff77aad05365fd0a1071c2" },
|
|
96
97
|
runtime: { count: 81, sha256: "99b2abff3ebff889b54b1781c563ab4b32609a479c4e90d6aad854f48fba7edc" },
|
|
97
98
|
reference: { count: 22, sha256: "243fd3c317553f8924c56a52c61af90ccf2793b7b0018af1373b73c69b6c3afb" },
|
|
98
|
-
state: { count:
|
|
99
|
+
state: { count: 38, sha256: "697de8dcd13ac521124c35058ba222aa5cb6cae546f00bb54652ceaca4b662aa" },
|
|
99
100
|
package: { count: 66, sha256: "83e6971af7f7564e42369aaccc445b32a0793bfffade843d149b6abd4fd3dbbc" },
|
|
100
101
|
bootstrap: { count: 34, sha256: "9a7dd7e27110d85cf5c08835fdd8f08119e75579858e63bc6d396c733961d0bc" },
|
|
101
102
|
},
|
|
102
|
-
total: { count:
|
|
103
|
+
total: { count: 280, sha256: "cd9396b45a82383e2f58da851f00061fa4c23c504ba9f41cf9fd7c7c39b0b092" },
|
|
103
104
|
});
|
|
104
105
|
export function canonicalTupleJson(value) { return JSON.stringify(value); }
|
|
105
106
|
export function digestCanonicalTuples(values) {
|
|
@@ -53,11 +53,11 @@ export const ACTIVATION_CENSUS_AUTHORITY = deepFreeze({
|
|
|
53
53
|
capability: { count: 12, sha256: "007e1157a892fec54182b1803aeaa442cf8e2e332e6a055c3bd020e2b0731867" },
|
|
54
54
|
runtime: { count: 81, sha256: "03fe600a3a27f05daf0d1b59c16b00bedc354a13c70ea60be0b457175fdf743c" },
|
|
55
55
|
reference: { count: 22, sha256: "2b48c82bb7ee10388d9ebb4fa7ea3743ea82a7e9aaedfcd5a858eeedb9d891ae" },
|
|
56
|
-
state: { count:
|
|
56
|
+
state: { count: 38, sha256: "7ab0dd6ef1b1b1ce66bd9ea94d1de3d542528233e928a44102e289bf12416681" },
|
|
57
57
|
package: { count: 66, sha256: "3548af7e84151c90690a6eb3d1cb6c7847f39b290161ecab599d8cb1bf0d2cb0" },
|
|
58
58
|
bootstrap: { count: 34, sha256: "71c2038744e2518a6adb722acbb5f9352bddfb5d1c92eeb0e347297ef2ca2f1e" },
|
|
59
59
|
},
|
|
60
|
-
total: { count:
|
|
60
|
+
total: { count: 280, sha256: "051ed9a57857a39e11ed8fb583bff9d2a67a306c5ab897a0ee829212468bc23a" },
|
|
61
61
|
});
|
|
62
62
|
/** Each dimension names the production contract it observes independently. */
|
|
63
63
|
export const ACTIVATION_EVIDENCE_SOURCES = deepFreeze({
|
|
@@ -71,12 +71,13 @@ export const ACTIVATION_EVIDENCE_SOURCES = deepFreeze({
|
|
|
71
71
|
});
|
|
72
72
|
export const ACTIVATION_CHECK_IDS = Object.freeze(ACTIVATION_CLASSES.flatMap((classId) => ACTIVATION_DIMENSIONS.map((dimension) => `${classId}.${dimension}`)));
|
|
73
73
|
export const SOURCE_GATE_IDS = [
|
|
74
|
-
"source", "stress", "performance", "package", "generated-overlap", "typecheck", "build",
|
|
74
|
+
"source", "stress", "performance", "capacity", "package", "generated-overlap", "typecheck", "build",
|
|
75
75
|
"compact", "capability-contract", "activation-conjunction",
|
|
76
76
|
];
|
|
77
77
|
export const SOURCE_DAG_PHASES = {
|
|
78
78
|
batchA: ["generated-overlap", "stress", "typecheck"],
|
|
79
79
|
performanceBarrier: ["performance"],
|
|
80
|
+
capacityBarrier: ["capacity"],
|
|
80
81
|
barrierB: ["compact", "capability-contract", "activation-conjunction"],
|
|
81
82
|
generatedOverlapOrigins: ["source", "package", "build", "generated-overlap"],
|
|
82
83
|
};
|
|
@@ -84,6 +85,7 @@ const EXACT_COMMANDS = {
|
|
|
84
85
|
source: ["pnpm", "-C", "packages/cli", "run", "test:source"],
|
|
85
86
|
stress: ["pnpm", "-C", "packages/cli", "run", "test:stress"],
|
|
86
87
|
performance: ["pnpm", "-C", "packages/cli", "run", "test:performance"],
|
|
88
|
+
capacity: ["pnpm", "-C", "packages/cli", "run", "test:capacity"],
|
|
87
89
|
package: ["pnpm", "-C", "packages/cli", "run", "verify:package"],
|
|
88
90
|
"generated-overlap": ["pnpm", "-C", "packages/cli", "run", "verify:generated-overlap"],
|
|
89
91
|
typecheck: ["pnpm", "-C", "packages/cli", "run", "typecheck"],
|
|
@@ -133,11 +135,33 @@ export function validatePackagePublicationDocument(raw) {
|
|
|
133
135
|
if (raw?.schemaVersion !== "agentera.packagePublication.v2")
|
|
134
136
|
fail("schemaVersion is invalid");
|
|
135
137
|
const source = raw?.qualification?.source;
|
|
138
|
+
const readiness = raw?.qualification?.readiness;
|
|
136
139
|
const activation = source?.activationConjunction;
|
|
137
140
|
if (!source || !activation || activation.gateIdentity !== "agentera.activationConjunction.v1")
|
|
138
141
|
fail("source activation conjunction is missing or invalid");
|
|
142
|
+
if (!readiness
|
|
143
|
+
|| readiness.schemaVersion !== "agentera.releaseReadiness.v1"
|
|
144
|
+
|| readiness.component !== "release-readiness"
|
|
145
|
+
|| readiness.adapter !== "development")
|
|
146
|
+
fail("release readiness authority is missing or invalid");
|
|
147
|
+
const readinessCommand = boundedText(readiness.command, "release readiness command", 320, /^node packages\/cli\/scripts\/release-readiness\.mjs development --candidate-dir DIR --target-version VERSION --source-commit COMMIT \[--metadata-commit COMMIT\] \[--json\]$/);
|
|
148
|
+
const readinessPhases = exactList(readiness.phases, ["source-readiness", "metadata-review", "candidate-readiness"], "release readiness phases");
|
|
149
|
+
if (!readiness.receipts
|
|
150
|
+
|| Object.keys(readiness.receipts).sort().join("\0") !== "candidate\0source"
|
|
151
|
+
|| readiness.receipts.source !== "source-receipt.json"
|
|
152
|
+
|| readiness.receipts.candidate !== "candidate-receipt.json")
|
|
153
|
+
fail("release readiness receipts must name the governed source and candidate receipts");
|
|
154
|
+
const readinessReuse = boundedText(readiness.reuse, "release readiness reuse rule", 1200, /^[^\0\r\n]+$/);
|
|
155
|
+
const readinessMetadataReview = boundedText(readiness.metadataReview, "release readiness metadata review rule", 1200, /^[^\0\r\n]+$/);
|
|
156
|
+
const readinessOutcomes = exactList(readiness.outcomes, ["paused", "ready", "rejected"], "release readiness outcomes");
|
|
157
|
+
if (!readiness.exitCodes
|
|
158
|
+
|| Object.keys(readiness.exitCodes).sort().join("\0") !== "paused\0ready\0rejected"
|
|
159
|
+
|| readiness.exitCodes.paused !== 0
|
|
160
|
+
|| readiness.exitCodes.ready !== 0
|
|
161
|
+
|| readiness.exitCodes.rejected !== 1)
|
|
162
|
+
fail("release readiness exit codes must map paused and ready to 0 and rejected to 1");
|
|
139
163
|
if (!Array.isArray(source.gates) || source.gates.length !== SOURCE_GATE_IDS.length)
|
|
140
|
-
fail(
|
|
164
|
+
fail(`source gates must contain exactly ${SOURCE_GATE_IDS.length} entries`);
|
|
141
165
|
const sourceGates = source.gates.map((gate, index) => {
|
|
142
166
|
const name = SOURCE_GATE_IDS[index];
|
|
143
167
|
if (!gate || gate.name !== name)
|
|
@@ -152,9 +176,10 @@ export function validatePackagePublicationDocument(raw) {
|
|
|
152
176
|
const dag = source.dag;
|
|
153
177
|
const batchA = exactList(dag?.batchA, SOURCE_DAG_PHASES.batchA, "source batch A");
|
|
154
178
|
const performanceBarrier = exactList(dag?.performanceBarrier, SOURCE_DAG_PHASES.performanceBarrier, "source performance barrier");
|
|
179
|
+
const capacityBarrier = exactList(dag?.capacityBarrier, SOURCE_DAG_PHASES.capacityBarrier, "source capacity barrier");
|
|
155
180
|
const barrierB = exactList(dag?.barrierB, SOURCE_DAG_PHASES.barrierB, "source barrier B");
|
|
156
181
|
const generatedOverlapOrigins = exactList(dag?.generatedOverlapOrigins, SOURCE_DAG_PHASES.generatedOverlapOrigins, "generated overlap origins");
|
|
157
|
-
const phases = [...batchA, ...performanceBarrier, ...barrierB];
|
|
182
|
+
const phases = [...batchA, ...performanceBarrier, ...capacityBarrier, ...barrierB];
|
|
158
183
|
if (phases.length !== new Set(phases).size)
|
|
159
184
|
fail("source execution phases must be disjoint");
|
|
160
185
|
const scheduled = new Set([...phases, ...generatedOverlapOrigins]);
|
|
@@ -211,12 +236,31 @@ export function validatePackagePublicationDocument(raw) {
|
|
|
211
236
|
return {
|
|
212
237
|
sourceGates,
|
|
213
238
|
sourceDag: {
|
|
214
|
-
batchA, performanceBarrier, barrierB, generatedOverlapOrigins,
|
|
239
|
+
batchA, performanceBarrier, capacityBarrier, barrierB, generatedOverlapOrigins,
|
|
215
240
|
overlapCleanupMarginMs: positiveInteger(dag.overlapCleanupMarginMs, "overlap cleanup margin", 60_000),
|
|
216
241
|
overlapParentReconciliationMarginMs: positiveInteger(dag.overlapParentReconciliationMarginMs, "parent reconciliation margin", 60_000),
|
|
217
242
|
minimumExecutionWindowMs,
|
|
218
243
|
},
|
|
219
|
-
sourceQualificationMs: positiveInteger(raw?.benchmark?.timeouts?.sourceQualificationMs, "source qualification timeout",
|
|
244
|
+
sourceQualificationMs: positiveInteger(raw?.benchmark?.timeouts?.sourceQualificationMs, "source qualification timeout", 2_400_000),
|
|
245
|
+
readiness: {
|
|
246
|
+
schemaVersion: readiness.schemaVersion,
|
|
247
|
+
component: readiness.component,
|
|
248
|
+
adapter: readiness.adapter,
|
|
249
|
+
command: readinessCommand,
|
|
250
|
+
phases: readinessPhases,
|
|
251
|
+
receipts: {
|
|
252
|
+
source: readiness.receipts.source,
|
|
253
|
+
candidate: readiness.receipts.candidate,
|
|
254
|
+
},
|
|
255
|
+
reuse: readinessReuse,
|
|
256
|
+
metadataReview: readinessMetadataReview,
|
|
257
|
+
outcomes: readinessOutcomes,
|
|
258
|
+
exitCodes: {
|
|
259
|
+
paused: readiness.exitCodes.paused,
|
|
260
|
+
ready: readiness.exitCodes.ready,
|
|
261
|
+
rejected: readiness.exitCodes.rejected,
|
|
262
|
+
},
|
|
263
|
+
},
|
|
220
264
|
activationConjunction: {
|
|
221
265
|
gateIdentity: activation.gateIdentity, classes, dimensions, checkIds, bounds, owners,
|
|
222
266
|
census: { algorithm: ACTIVATION_CENSUS_AUTHORITY.algorithm, classes: censusClasses, total: censusTotal },
|
|
@@ -22,18 +22,12 @@ import { canonicalMigrationRecord } from "./canonicalMigrationRecord.js";
|
|
|
22
22
|
import { legacySummaryRecord } from "./legacySummaryRecord.js";
|
|
23
23
|
import { applyCausalBlockers } from "./entityMigrationCausality.js";
|
|
24
24
|
import { todoMigrationObservations, todoReconciliationMigrationPlan } from "./entityMigrationTodo.js";
|
|
25
|
-
import {
|
|
25
|
+
import { classifyProjectState } from "./stateMode.js";
|
|
26
26
|
import { TODO_RECONCILIATION_ACTIVATION_PATH } from "./todoReconciliationActivation.js";
|
|
27
27
|
import { projectPathIsStable as migrationPathIsStable, readProjectFileSnapshot, resolveProjectDescriptorPath, snapshotProjectPath as snapshotMigrationPath, } from "./safeProjectFile.js";
|
|
28
28
|
export function classifyEntityCutoverProject(project, sourceRoot) {
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
const stateRoot = path.join(project, ".agentera");
|
|
32
|
-
const aggregateNames = ["plan.yaml", "progress.yaml", "decisions.yaml", "health.yaml"];
|
|
33
|
-
const aggregateCount = aggregateNames.filter((name) => fs.existsSync(path.join(stateRoot, name))).length;
|
|
34
|
-
if (!fs.existsSync(stateRoot) || aggregateCount === 0 && !fs.existsSync(path.join(stateRoot, "entities")))
|
|
35
|
-
return "clean";
|
|
36
|
-
return fs.existsSync(path.join(stateRoot, "entities")) || aggregateCount === 1 ? "partial" : "v2";
|
|
29
|
+
const state = classifyProjectState(project, sourceRoot).state;
|
|
30
|
+
return state === "entities" ? "v3" : state;
|
|
37
31
|
}
|
|
38
32
|
export const ENTITY_MIGRATION_PREVIEW_MAX_OUTPUT_BYTES = 32_768;
|
|
39
33
|
const DEFAULT_LIMIT = 100;
|
|
@@ -245,6 +245,14 @@ function publicationConflict(markerPath) {
|
|
|
245
245
|
example: `restore the exact '${markerPath}' marker selected at command start and retry`,
|
|
246
246
|
});
|
|
247
247
|
}
|
|
248
|
+
function initializationConflict(markerPath) {
|
|
249
|
+
reject({
|
|
250
|
+
class: "conflict",
|
|
251
|
+
message: `state mode marker '${markerPath}' appeared during fresh plan initialization`,
|
|
252
|
+
syntax: "agentera state plan create --input PLAN.yaml --format json",
|
|
253
|
+
example: "remove no files; inspect the competing state publication and retry",
|
|
254
|
+
});
|
|
255
|
+
}
|
|
248
256
|
/**
|
|
249
257
|
* Binds mutations to a validated project and exact entity-mode marker. Every
|
|
250
258
|
* pathname and byte identity is checked at declared boundaries. Publication
|
|
@@ -264,14 +272,24 @@ export class EntityPublicationContext {
|
|
|
264
272
|
this.validatedRoot = root;
|
|
265
273
|
this.markerPath = markerPath;
|
|
266
274
|
const markerAbsolute = path.join(root.path, ...safeSegments(markerPath, "state mode marker"));
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
275
|
+
if (expectedBytes === null) {
|
|
276
|
+
if (readFileIfPresent(markerAbsolute, `state mode marker '${markerPath}'`))
|
|
277
|
+
initializationConflict(markerPath);
|
|
278
|
+
this.marker = null;
|
|
279
|
+
}
|
|
280
|
+
else {
|
|
281
|
+
this.marker = readStableFile(markerAbsolute, `state mode marker '${markerPath}'`);
|
|
282
|
+
if (!this.marker.bytes.equals(expectedBytes))
|
|
283
|
+
publicationConflict(markerPath);
|
|
284
|
+
}
|
|
270
285
|
this.assertValid();
|
|
271
286
|
}
|
|
272
287
|
static open(root, markerPath, expectedBytes) {
|
|
273
288
|
return new EntityPublicationContext(root, markerPath, expectedBytes);
|
|
274
289
|
}
|
|
290
|
+
static beginInitialization(root, markerPath) {
|
|
291
|
+
return new EntityPublicationContext(root, markerPath, null);
|
|
292
|
+
}
|
|
275
293
|
pinnedPath(relativePath = "") {
|
|
276
294
|
if (this.closed)
|
|
277
295
|
throw new Error("entity publication context is closed");
|
|
@@ -283,6 +301,17 @@ export class EntityPublicationContext {
|
|
|
283
301
|
if (this.closed)
|
|
284
302
|
throw new Error("entity publication context is closed");
|
|
285
303
|
assertValidatedProjectRoot(this.validatedRoot);
|
|
304
|
+
if (this.marker === null) {
|
|
305
|
+
try {
|
|
306
|
+
if (readFileIfPresent(path.join(this.projectRoot, ...safeSegments(this.markerPath, "state mode marker")), `state mode marker '${this.markerPath}'`)) {
|
|
307
|
+
initializationConflict(this.markerPath);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
catch {
|
|
311
|
+
initializationConflict(this.markerPath);
|
|
312
|
+
}
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
286
315
|
let current;
|
|
287
316
|
try {
|
|
288
317
|
current = readStableFile(this.marker.absolute, `state mode marker '${this.markerPath}'`);
|
|
@@ -294,9 +323,25 @@ export class EntityPublicationContext {
|
|
|
294
323
|
publicationConflict(this.markerPath);
|
|
295
324
|
}
|
|
296
325
|
publishImmutable(relativeTarget, bytes) {
|
|
326
|
+
return this.publishImmutableInternal(relativeTarget, bytes, false);
|
|
327
|
+
}
|
|
328
|
+
isInitializing() {
|
|
329
|
+
return this.marker === null;
|
|
330
|
+
}
|
|
331
|
+
publishStateMarker(bytes) {
|
|
332
|
+
if (this.marker !== null)
|
|
333
|
+
throw new Error("entity state marker is already active");
|
|
334
|
+
const published = this.publishImmutableInternal(this.markerPath, bytes, true);
|
|
335
|
+
if (!published)
|
|
336
|
+
initializationConflict(this.markerPath);
|
|
337
|
+
return published;
|
|
338
|
+
}
|
|
339
|
+
publishImmutableInternal(relativeTarget, bytes, activateMarker) {
|
|
297
340
|
const segments = safeSegments(relativeTarget, "immutable entity target");
|
|
298
341
|
if (segments.length < 2)
|
|
299
342
|
throw new Error(`unsafe immutable entity target '${relativeTarget}'`);
|
|
343
|
+
if (activateMarker && relativeTarget !== this.markerPath)
|
|
344
|
+
throw new Error("only the state mode marker can activate fresh entity state");
|
|
300
345
|
const directories = [];
|
|
301
346
|
const target = path.join(this.projectRoot, ...segments);
|
|
302
347
|
let stage;
|
|
@@ -338,11 +383,15 @@ export class EntityPublicationContext {
|
|
|
338
383
|
throw new ExactReplacementConflictError(`immutable entity target '${relativeTarget}' changed at its publication boundary`);
|
|
339
384
|
}
|
|
340
385
|
publicationOwned = true;
|
|
386
|
+
if (activateMarker)
|
|
387
|
+
this.marker = published;
|
|
341
388
|
this.assertBoundary(directories, published, [stage]);
|
|
342
389
|
removeExactFile(stage);
|
|
343
390
|
stage = undefined;
|
|
344
391
|
syncDirectory(directory);
|
|
345
392
|
published = readStableFile(target, `published entity '${relativeTarget}'`);
|
|
393
|
+
if (activateMarker)
|
|
394
|
+
this.marker = published;
|
|
346
395
|
this.assertBoundary(directories, published);
|
|
347
396
|
for (const entry of directories.filter(({ created }) => created)) {
|
|
348
397
|
this.createdDirectories.set(relativeDisplay(this.projectRoot, entry.absolute), entry);
|
|
@@ -350,6 +399,8 @@ export class EntityPublicationContext {
|
|
|
350
399
|
return publishedIdentity(published);
|
|
351
400
|
}
|
|
352
401
|
catch (error) {
|
|
402
|
+
if (activateMarker)
|
|
403
|
+
this.marker = null;
|
|
353
404
|
if (publicationOwned && published) {
|
|
354
405
|
if (stage) {
|
|
355
406
|
try {
|
|
@@ -434,6 +485,8 @@ export class EntityPublicationContext {
|
|
|
434
485
|
return "identity_mismatch";
|
|
435
486
|
fs.unlinkSync(target);
|
|
436
487
|
syncDirectory(directory);
|
|
488
|
+
if (relativeTarget === this.markerPath)
|
|
489
|
+
this.marker = null;
|
|
437
490
|
this.removeAttemptDirectories();
|
|
438
491
|
return "removed";
|
|
439
492
|
}
|
|
@@ -6,8 +6,9 @@ import { dumpYamlMapping } from "../core/yaml.js";
|
|
|
6
6
|
import { canonicalRecordJson } from "./archiveDiscovery.js";
|
|
7
7
|
import { StateRetrievalFailure } from "./directRetrieval.js";
|
|
8
8
|
import { allocateEntityId, canonicalEntityEnvelopeBytes, entityExactGetMaxBytes, exactDiscoveredEntityBytes, publishEntity, replaceEntity, replaceEntityUnderLock, validateEntityDiscovery, validateEntityState, withEntityWriterLock } from "./entityStorage.js";
|
|
9
|
+
import { EntityPublicationContext } from "./entityPublicationContext.js";
|
|
9
10
|
import { inspectPendingPlanReplacement, publishPlanReplacement, recoverPendingPlanReplacement } from "./planReplacementTransaction.js";
|
|
10
|
-
import { detectStateModeBinding } from "./stateMode.js";
|
|
11
|
+
import { detectStateModeBinding, freshEntityStateMarker, freshPlanInitialization } from "./stateMode.js";
|
|
11
12
|
import { normalizeAndValidatePlanCreateInput, validatePlanPublicationCandidate } from "./write/planPublication.js";
|
|
12
13
|
import { reject } from "./write/errors.js";
|
|
13
14
|
import { mutatePlanTaskEvaluation, planTaskRecordViolations } from "./write/planEvaluation.js";
|
|
@@ -249,8 +250,24 @@ export function createPlanEntities(req, options = {}) {
|
|
|
249
250
|
}
|
|
250
251
|
return withEntityWriterLock(options.publicationContext, () => createPlanEntitiesUnderLock(req, options));
|
|
251
252
|
}
|
|
253
|
+
/** The first Plan is the sole marker-absent entity initializer. */
|
|
254
|
+
export function createFreshPlanEntities(req, options = {}) {
|
|
255
|
+
const sourceRoot = options.sourceRoot ?? resolveSourceRoot();
|
|
256
|
+
const initialization = freshPlanInitialization(req.projectRoot, sourceRoot);
|
|
257
|
+
if (!initialization)
|
|
258
|
+
return null;
|
|
259
|
+
const publicationContext = EntityPublicationContext.beginInitialization(initialization.root, initialization.markerPath);
|
|
260
|
+
try {
|
|
261
|
+
return createPlanEntities(req, { ...options, sourceRoot, publicationContext });
|
|
262
|
+
}
|
|
263
|
+
finally {
|
|
264
|
+
publicationContext.close();
|
|
265
|
+
}
|
|
266
|
+
}
|
|
252
267
|
function createPlanEntitiesUnderLock(req, options) {
|
|
253
268
|
const sourceRoot = options.sourceRoot ?? resolveSourceRoot();
|
|
269
|
+
const initializing = options.publicationContext.isInitializing();
|
|
270
|
+
const marker = initializing ? freshEntityStateMarker(sourceRoot) : null;
|
|
254
271
|
const input = preparedPlanCreateInput(req);
|
|
255
272
|
const discovery = validateEntityState(options.publicationContext.pinnedPath(), sourceRoot, { kind: "project", projectRoot: options.publicationContext.validatedRoot });
|
|
256
273
|
const entities = all(options.publicationContext.pinnedPath(), sourceRoot, discovery);
|
|
@@ -292,8 +309,21 @@ function createPlanEntitiesUnderLock(req, options) {
|
|
|
292
309
|
return record;
|
|
293
310
|
});
|
|
294
311
|
const publications = [{ boundary: PLAN, id: planId, record: planRecord }, ...taskRecords.map((record, index) => ({ boundary: TASK, id: taskIds[index], record }))];
|
|
312
|
+
const resultExtra = {
|
|
313
|
+
tasks: taskRecords.map((record, index) => ({ id: taskIds[index], artifact: ARTIFACT, record })),
|
|
314
|
+
effects: lifecycle.effects,
|
|
315
|
+
...(marker ? {
|
|
316
|
+
initialization: {
|
|
317
|
+
atomic: true,
|
|
318
|
+
marker: {
|
|
319
|
+
path: path.join(req.projectRoot, options.publicationContext.markerPath),
|
|
320
|
+
record: marker,
|
|
321
|
+
},
|
|
322
|
+
},
|
|
323
|
+
} : {}),
|
|
324
|
+
};
|
|
295
325
|
if (req.dryRun)
|
|
296
|
-
return envelope(command, { id: planId, path: entityPath(req.projectRoot, sourceRoot, PLAN, planId), replay: false }, planRecord, true,
|
|
326
|
+
return envelope(command, { id: planId, path: entityPath(req.projectRoot, sourceRoot, PLAN, planId), replay: false }, planRecord, true, resultExtra);
|
|
297
327
|
if (command === "state plan replace" && lifecycle.predecessor && lifecycle.archivedRecord && lifecycle.replacementInputSha256) {
|
|
298
328
|
const targets = [
|
|
299
329
|
{
|
|
@@ -314,10 +344,11 @@ function createPlanEntitiesUnderLock(req, options) {
|
|
|
314
344
|
throw new Error(`targeted plan replacement graph failed state validation: ${validation.issues.map(({ message }) => message).join("; ")}`);
|
|
315
345
|
},
|
|
316
346
|
});
|
|
317
|
-
return envelope(command, { id: planId, path: entityPath(req.projectRoot, sourceRoot, PLAN, planId), replay: false }, planRecord, false,
|
|
347
|
+
return envelope(command, { id: planId, path: entityPath(req.projectRoot, sourceRoot, PLAN, planId), replay: false }, planRecord, false, resultExtra);
|
|
318
348
|
}
|
|
319
349
|
const published = [];
|
|
320
350
|
let predecessor;
|
|
351
|
+
let markerIdentity;
|
|
321
352
|
try {
|
|
322
353
|
if (lifecycle.predecessor && lifecycle.archivedRecord) {
|
|
323
354
|
const archived = replaceEntityUnderLock({ projectRoot: req.projectRoot, sourceRoot, publicationContext: options.publicationContext, artifact: ARTIFACT, boundary: PLAN, id: lifecycle.predecessor.id, expectedRecord: lifecycle.predecessor.record, expectedBytes: exactDiscoveredEntityBytes(lifecycle.predecessor), migrationProvenance: lifecycle.predecessor.migrationProvenance, record: lifecycle.archivedRecord });
|
|
@@ -338,6 +369,10 @@ function createPlanEntitiesUnderLock(req, options) {
|
|
|
338
369
|
if (!validation.valid)
|
|
339
370
|
throw new Error(`created plan graph failed state validation: ${validation.issues.map(({ message }) => message).join("; ")}`);
|
|
340
371
|
options.publicationContext.assertValid();
|
|
372
|
+
if (marker) {
|
|
373
|
+
markerIdentity = options.publicationContext.publishStateMarker(dumpYamlMapping(marker));
|
|
374
|
+
options.publicationContext.assertValid();
|
|
375
|
+
}
|
|
341
376
|
}
|
|
342
377
|
catch (error) {
|
|
343
378
|
const recoveryFailures = [];
|
|
@@ -359,11 +394,21 @@ function createPlanEntitiesUnderLock(req, options) {
|
|
|
359
394
|
recoveryFailures.push(`predecessor restoration failed because ownership changed or publication was unsafe: ${restoreError.message}`);
|
|
360
395
|
}
|
|
361
396
|
}
|
|
397
|
+
if (markerIdentity) {
|
|
398
|
+
try {
|
|
399
|
+
const removed = options.publicationContext.removeExact(options.publicationContext.markerPath, markerIdentity, false);
|
|
400
|
+
if (removed === "identity_mismatch")
|
|
401
|
+
recoveryFailures.push("cleanup ownership changed for fresh state marker");
|
|
402
|
+
}
|
|
403
|
+
catch (cleanupError) {
|
|
404
|
+
recoveryFailures.push(`cleanup failed for fresh state marker: ${cleanupError.message}`);
|
|
405
|
+
}
|
|
406
|
+
}
|
|
362
407
|
if (recoveryFailures.length)
|
|
363
408
|
throw new Error(`plan replacement failed: ${error.message}; recovery failed: ${recoveryFailures.join("; ")}`, { cause: error });
|
|
364
409
|
throw error;
|
|
365
410
|
}
|
|
366
|
-
return envelope(command, { id: planId, path: entityPath(req.projectRoot, sourceRoot, PLAN, planId), replay: false }, planRecord, false,
|
|
411
|
+
return envelope(command, { id: planId, path: entityPath(req.projectRoot, sourceRoot, PLAN, planId), replay: false }, planRecord, false, resultExtra);
|
|
367
412
|
}
|
|
368
413
|
function replacementEffects(predecessor, successor) {
|
|
369
414
|
return {
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
const APPEND_GUIDANCE = "Append progress only for durable project truth that future work needs, a required glossary_caveat lifecycle event, or a plan-completion sweep. Glossary caveats and plan-completion sweeps require a record; an ordinary build or release attempt with no durable project truth change, and an outcome future work does not need as a milestone, require no progress append.";
|
|
2
|
+
const RECEIPT_GUIDANCE = "Qualification and publication receipts own timings, integrity, digests, retries, and replay; do not duplicate receipt detail in progress.";
|
|
3
|
+
const PROGRESS_WRITE_POLICY = {
|
|
4
|
+
schemaVersion: "agentera.progressWritePolicy.v1",
|
|
5
|
+
append: {
|
|
6
|
+
mode: "conditional",
|
|
7
|
+
allowed_when: [
|
|
8
|
+
"durable_project_truth_needed_by_future_work",
|
|
9
|
+
"required_glossary_caveat",
|
|
10
|
+
"plan_completion_sweep",
|
|
11
|
+
],
|
|
12
|
+
required_when: [
|
|
13
|
+
"required_glossary_caveat",
|
|
14
|
+
"plan_completion_sweep",
|
|
15
|
+
],
|
|
16
|
+
no_append_required_when: [
|
|
17
|
+
"ordinary_build_or_release_attempt_without_durable_project_truth_change",
|
|
18
|
+
"durable_outcome_not_needed_by_future_work",
|
|
19
|
+
],
|
|
20
|
+
guidance: APPEND_GUIDANCE,
|
|
21
|
+
},
|
|
22
|
+
receipt_detail: {
|
|
23
|
+
owners: ["qualification_receipt", "publication_receipt"],
|
|
24
|
+
fields: ["timings", "integrity", "digests", "retries", "replay"],
|
|
25
|
+
guidance: RECEIPT_GUIDANCE,
|
|
26
|
+
},
|
|
27
|
+
};
|
|
28
|
+
export function progressWritePolicy() {
|
|
29
|
+
return structuredClone(PROGRESS_WRITE_POLICY);
|
|
30
|
+
}
|
|
31
|
+
export function progressWriteGuidance() {
|
|
32
|
+
return [APPEND_GUIDANCE, RECEIPT_GUIDANCE];
|
|
33
|
+
}
|