@orion-agents/orion-code 0.1.0 → 0.1.2
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 +107 -67
- package/README.zh-CN.md +192 -217
- package/dist/cli.js +23 -21
- package/dist/cli.js.map +1 -1
- package/dist/commands/index.d.ts +1 -1
- package/dist/commands/index.d.ts.map +1 -1
- package/dist/commands/index.js +397 -212
- package/dist/commands/index.js.map +1 -1
- package/dist/commands/target-command.d.ts.map +1 -1
- package/dist/commands/target-command.js +43 -9
- package/dist/commands/target-command.js.map +1 -1
- package/dist/commands/types.d.ts +33 -0
- package/dist/commands/types.d.ts.map +1 -1
- package/dist/commands/types.js +5 -1
- package/dist/commands/types.js.map +1 -1
- package/dist/core/agent.d.ts.map +1 -1
- package/dist/core/agent.js +2 -2
- package/dist/core/agent.js.map +1 -1
- package/dist/framework/external-assertion.d.ts +65 -0
- package/dist/framework/external-assertion.d.ts.map +1 -0
- package/dist/framework/external-assertion.js +571 -0
- package/dist/framework/external-assertion.js.map +1 -0
- package/dist/framework/index.d.ts +6 -4
- package/dist/framework/index.d.ts.map +1 -1
- package/dist/framework/index.js +4 -1
- package/dist/framework/index.js.map +1 -1
- package/dist/framework/query.d.ts +18 -1
- package/dist/framework/query.d.ts.map +1 -1
- package/dist/framework/query.js +65 -28
- package/dist/framework/query.js.map +1 -1
- package/dist/framework/tool-scheduler.d.ts +2 -0
- package/dist/framework/tool-scheduler.d.ts.map +1 -1
- package/dist/framework/tool-scheduler.js +23 -9
- package/dist/framework/tool-scheduler.js.map +1 -1
- package/dist/framework/tool-serializer.d.ts.map +1 -1
- package/dist/framework/tool-serializer.js +20 -4
- package/dist/framework/tool-serializer.js.map +1 -1
- package/dist/framework/tool.d.ts +11 -5
- package/dist/framework/tool.d.ts.map +1 -1
- package/dist/framework/tool.js.map +1 -1
- package/dist/harness/context-harness.d.ts.map +1 -1
- package/dist/harness/context-harness.js +4 -3
- package/dist/harness/context-harness.js.map +1 -1
- package/dist/harness/drift-guard.d.ts.map +1 -1
- package/dist/harness/drift-guard.js +14 -5
- package/dist/harness/drift-guard.js.map +1 -1
- package/dist/harness/safety.d.ts.map +1 -1
- package/dist/harness/safety.js +19 -3
- package/dist/harness/safety.js.map +1 -1
- package/dist/init.d.ts +1 -1
- package/dist/init.d.ts.map +1 -1
- package/dist/init.js +115 -34
- package/dist/init.js.map +1 -1
- package/dist/ink-ui/screens/ReplScreen.d.ts.map +1 -1
- package/dist/ink-ui/screens/ReplScreen.js +4 -1
- package/dist/ink-ui/screens/ReplScreen.js.map +1 -1
- package/dist/memory/storage.d.ts.map +1 -1
- package/dist/memory/storage.js +1 -0
- package/dist/memory/storage.js.map +1 -1
- package/dist/memory/store.d.ts.map +1 -1
- package/dist/memory/store.js +11 -5
- package/dist/memory/store.js.map +1 -1
- package/dist/migration/command.d.ts.map +1 -1
- package/dist/migration/command.js +6 -1
- package/dist/migration/command.js.map +1 -1
- package/dist/migration/migrate.d.ts.map +1 -1
- package/dist/migration/migrate.js +4 -1
- package/dist/migration/migrate.js.map +1 -1
- package/dist/print-ui/launch.d.ts +4 -0
- package/dist/print-ui/launch.d.ts.map +1 -1
- package/dist/print-ui/launch.js +102 -9
- package/dist/print-ui/launch.js.map +1 -1
- package/dist/product/identity.d.ts +2 -2
- package/dist/product/identity.d.ts.map +1 -1
- package/dist/product/identity.js +2 -2
- package/dist/product/identity.js.map +1 -1
- package/dist/product/index.d.ts +2 -1
- package/dist/product/index.d.ts.map +1 -1
- package/dist/product/index.js +5 -1
- package/dist/product/index.js.map +1 -1
- package/dist/product/version.d.ts +12 -0
- package/dist/product/version.d.ts.map +1 -0
- package/dist/product/version.js +31 -0
- package/dist/product/version.js.map +1 -0
- package/dist/runtime/agent-runtime-controller.d.ts +49 -0
- package/dist/runtime/agent-runtime-controller.d.ts.map +1 -1
- package/dist/runtime/agent-runtime-controller.js +1048 -81
- package/dist/runtime/agent-runtime-controller.js.map +1 -1
- package/dist/runtime/agent-runtime-protocol.d.ts +14 -2
- package/dist/runtime/agent-runtime-protocol.d.ts.map +1 -1
- package/dist/runtime/agent-runtime-protocol.js +18 -0
- package/dist/runtime/agent-runtime-protocol.js.map +1 -1
- package/dist/runtime/chat-controller.d.ts +6 -0
- package/dist/runtime/chat-controller.d.ts.map +1 -1
- package/dist/runtime/chat-controller.js +226 -20
- package/dist/runtime/chat-controller.js.map +1 -1
- package/dist/runtime/goals/accounting.d.ts +28 -0
- package/dist/runtime/goals/accounting.d.ts.map +1 -1
- package/dist/runtime/goals/accounting.js +69 -0
- package/dist/runtime/goals/accounting.js.map +1 -1
- package/dist/runtime/goals/completion-audit.d.ts +13 -8
- package/dist/runtime/goals/completion-audit.d.ts.map +1 -1
- package/dist/runtime/goals/completion-audit.js +598 -16
- package/dist/runtime/goals/completion-audit.js.map +1 -1
- package/dist/runtime/goals/coordinator.d.ts +44 -5
- package/dist/runtime/goals/coordinator.d.ts.map +1 -1
- package/dist/runtime/goals/coordinator.js +1124 -58
- package/dist/runtime/goals/coordinator.js.map +1 -1
- package/dist/runtime/goals/evidence.d.ts +37 -0
- package/dist/runtime/goals/evidence.d.ts.map +1 -0
- package/dist/runtime/goals/evidence.js +300 -0
- package/dist/runtime/goals/evidence.js.map +1 -0
- package/dist/runtime/goals/presentation.d.ts +4 -0
- package/dist/runtime/goals/presentation.d.ts.map +1 -0
- package/dist/runtime/goals/presentation.js +50 -0
- package/dist/runtime/goals/presentation.js.map +1 -0
- package/dist/runtime/goals/prompt.d.ts.map +1 -1
- package/dist/runtime/goals/prompt.js +38 -8
- package/dist/runtime/goals/prompt.js.map +1 -1
- package/dist/runtime/goals/tools.d.ts +18 -1
- package/dist/runtime/goals/tools.d.ts.map +1 -1
- package/dist/runtime/goals/tools.js +308 -28
- package/dist/runtime/goals/tools.js.map +1 -1
- package/dist/runtime/goals/types.d.ts +241 -7
- package/dist/runtime/goals/types.d.ts.map +1 -1
- package/dist/runtime/goals/types.js +8 -2
- package/dist/runtime/goals/types.js.map +1 -1
- package/dist/runtime/subagents/child-executor-guard.d.ts +12 -4
- package/dist/runtime/subagents/child-executor-guard.d.ts.map +1 -1
- package/dist/runtime/subagents/child-executor-guard.js +397 -61
- package/dist/runtime/subagents/child-executor-guard.js.map +1 -1
- package/dist/runtime/subagents/presets.d.ts +4 -5
- package/dist/runtime/subagents/presets.d.ts.map +1 -1
- package/dist/runtime/subagents/presets.js +18 -33
- package/dist/runtime/subagents/presets.js.map +1 -1
- package/dist/runtime/subagents/production.d.ts +3 -1
- package/dist/runtime/subagents/production.d.ts.map +1 -1
- package/dist/runtime/subagents/production.js +37 -7
- package/dist/runtime/subagents/production.js.map +1 -1
- package/dist/runtime/subagents/runner.d.ts.map +1 -1
- package/dist/runtime/subagents/runner.js +24 -3
- package/dist/runtime/subagents/runner.js.map +1 -1
- package/dist/runtime/subagents/runtime-integration.d.ts +3 -0
- package/dist/runtime/subagents/runtime-integration.d.ts.map +1 -1
- package/dist/runtime/subagents/runtime-integration.js +18 -7
- package/dist/runtime/subagents/runtime-integration.js.map +1 -1
- package/dist/runtime/subagents/supervisor.d.ts +4 -4
- package/dist/runtime/subagents/supervisor.d.ts.map +1 -1
- package/dist/runtime/subagents/supervisor.js +39 -19
- package/dist/runtime/subagents/supervisor.js.map +1 -1
- package/dist/runtime/subagents/types.d.ts +7 -0
- package/dist/runtime/subagents/types.d.ts.map +1 -1
- package/dist/runtime/subagents/types.js +12 -1
- package/dist/runtime/subagents/types.js.map +1 -1
- package/dist/runtime/ui-events.d.ts +10 -0
- package/dist/runtime/ui-events.d.ts.map +1 -1
- package/dist/runtime/ui-events.js +5 -5
- package/dist/runtime/ui-events.js.map +1 -1
- package/dist/runtime/ui-view-model.d.ts +3 -3
- package/dist/runtime/ui-view-model.d.ts.map +1 -1
- package/dist/runtime/ui-view-model.js +11 -4
- package/dist/runtime/ui-view-model.js.map +1 -1
- package/dist/services/config.d.ts +18 -7
- package/dist/services/config.d.ts.map +1 -1
- package/dist/services/config.js +79 -57
- package/dist/services/config.js.map +1 -1
- package/dist/services/doctor.d.ts.map +1 -1
- package/dist/services/doctor.js +164 -18
- package/dist/services/doctor.js.map +1 -1
- package/dist/services/global-config.d.ts +1 -1
- package/dist/services/global-config.d.ts.map +1 -1
- package/dist/services/goal-storage.d.ts +9 -4
- package/dist/services/goal-storage.d.ts.map +1 -1
- package/dist/services/goal-storage.js +1113 -75
- package/dist/services/goal-storage.js.map +1 -1
- package/dist/services/llm.d.ts +19 -0
- package/dist/services/llm.d.ts.map +1 -1
- package/dist/services/llm.js +153 -20
- package/dist/services/llm.js.map +1 -1
- package/dist/services/model-registry.d.ts +1 -1
- package/dist/services/model-registry.d.ts.map +1 -1
- package/dist/services/model-registry.js +69 -19
- package/dist/services/model-registry.js.map +1 -1
- package/dist/services/provider-resilience/coordinator.d.ts +2 -1
- package/dist/services/provider-resilience/coordinator.d.ts.map +1 -1
- package/dist/services/provider-resilience/coordinator.js +7 -6
- package/dist/services/provider-resilience/coordinator.js.map +1 -1
- package/dist/services/provider-resilience/error-classifier.d.ts.map +1 -1
- package/dist/services/provider-resilience/error-classifier.js +67 -17
- package/dist/services/provider-resilience/error-classifier.js.map +1 -1
- package/dist/services/provider-resilience/types.d.ts +1 -1
- package/dist/services/provider-resilience/types.d.ts.map +1 -1
- package/dist/services/provider-resilience/types.js.map +1 -1
- package/dist/services/redaction.d.ts.map +1 -1
- package/dist/services/redaction.js +9 -1
- package/dist/services/redaction.js.map +1 -1
- package/dist/services/session-storage.d.ts +12 -1
- package/dist/services/session-storage.d.ts.map +1 -1
- package/dist/services/session-storage.js +23 -7
- package/dist/services/session-storage.js.map +1 -1
- package/dist/services/storage-maintenance.d.ts +73 -3
- package/dist/services/storage-maintenance.d.ts.map +1 -1
- package/dist/services/storage-maintenance.js +771 -104
- package/dist/services/storage-maintenance.js.map +1 -1
- package/dist/services/task-manager.d.ts.map +1 -1
- package/dist/services/task-manager.js +2 -2
- package/dist/services/task-manager.js.map +1 -1
- package/dist/services/web-search-adapters.d.ts.map +1 -1
- package/dist/services/web-search-adapters.js +89 -49
- package/dist/services/web-search-adapters.js.map +1 -1
- package/dist/services/web-search-mcp.d.ts.map +1 -1
- package/dist/services/web-search-mcp.js +34 -37
- package/dist/services/web-search-mcp.js.map +1 -1
- package/dist/services/workspace-state.d.ts +4 -0
- package/dist/services/workspace-state.d.ts.map +1 -1
- package/dist/services/workspace-state.js +30 -9
- package/dist/services/workspace-state.js.map +1 -1
- package/dist/terminal-ui/completion.js +1 -1
- package/dist/terminal-ui/completion.js.map +1 -1
- package/dist/terminal-ui/launch.d.ts +3 -0
- package/dist/terminal-ui/launch.d.ts.map +1 -1
- package/dist/terminal-ui/launch.js +26 -98
- package/dist/terminal-ui/launch.js.map +1 -1
- package/dist/tools/git.d.ts.map +1 -1
- package/dist/tools/git.js +275 -27
- package/dist/tools/git.js.map +1 -1
- package/dist/tools/index.d.ts +2 -0
- package/dist/tools/index.d.ts.map +1 -1
- package/dist/tools/index.js +29 -10
- package/dist/tools/index.js.map +1 -1
- package/dist/tools/mcp.d.ts.map +1 -1
- package/dist/tools/mcp.js +20 -12
- package/dist/tools/mcp.js.map +1 -1
- package/dist/tools/web.d.ts.map +1 -1
- package/dist/tools/web.js +38 -14
- package/dist/tools/web.js.map +1 -1
- package/dist/tui-ui/layout.d.ts.map +1 -1
- package/dist/tui-ui/layout.js +36 -23
- package/dist/tui-ui/layout.js.map +1 -1
- package/dist/tui-ui/pickers.d.ts.map +1 -1
- package/dist/tui-ui/pickers.js +5 -4
- package/dist/tui-ui/pickers.js.map +1 -1
- package/dist/tui-ui/runner.d.ts +1 -0
- package/dist/tui-ui/runner.d.ts.map +1 -1
- package/dist/tui-ui/runner.js +15 -17
- package/dist/tui-ui/runner.js.map +1 -1
- package/dist/tui-ui/state.d.ts +6 -0
- package/dist/tui-ui/state.d.ts.map +1 -1
- package/dist/tui-ui/state.js +61 -30
- package/dist/tui-ui/state.js.map +1 -1
- package/package.json +14 -9
|
@@ -8,31 +8,442 @@
|
|
|
8
8
|
*/
|
|
9
9
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
10
10
|
exports.GoalCoordinator = void 0;
|
|
11
|
+
exports.goalRequiresBoundaryConfirmation = goalRequiresBoundaryConfirmation;
|
|
12
|
+
const crypto_1 = require("crypto");
|
|
13
|
+
const fs_1 = require("fs");
|
|
14
|
+
const path_1 = require("path");
|
|
11
15
|
const types_1 = require("./types");
|
|
12
16
|
const goal_storage_1 = require("../../services/goal-storage");
|
|
13
17
|
const accounting_1 = require("./accounting");
|
|
14
18
|
const completion_audit_1 = require("./completion-audit");
|
|
19
|
+
const redaction_1 = require("../../services/redaction");
|
|
20
|
+
const config_dir_1 = require("../../services/config-dir");
|
|
21
|
+
const MAX_EVIDENCE_LEDGER_RECORDS = 500;
|
|
22
|
+
const MAX_EVIDENCE_TRUNCATION_COUNT = 1000000000;
|
|
23
|
+
function addSafeInteger(value, increment, field) {
|
|
24
|
+
if (!Number.isSafeInteger(value) ||
|
|
25
|
+
value < 0 ||
|
|
26
|
+
!Number.isSafeInteger(increment) ||
|
|
27
|
+
increment < 0 ||
|
|
28
|
+
value > Number.MAX_SAFE_INTEGER - increment) {
|
|
29
|
+
throw new RangeError(`${field} would exceed the non-negative safe integer range`);
|
|
30
|
+
}
|
|
31
|
+
return value + increment;
|
|
32
|
+
}
|
|
33
|
+
function boundedTruncationCount(current, increment) {
|
|
34
|
+
return Math.min(MAX_EVIDENCE_TRUNCATION_COUNT, current + increment);
|
|
35
|
+
}
|
|
36
|
+
function appendBoundedEvidence(existing, incoming, objectiveRevision, previousTruncation) {
|
|
37
|
+
const all = [...existing, ...incoming];
|
|
38
|
+
const carried = previousTruncation?.objectiveRevision === objectiveRevision ? previousTruncation : undefined;
|
|
39
|
+
if (all.length <= MAX_EVIDENCE_LEDGER_RECORDS) {
|
|
40
|
+
return { ledger: all, truncation: carried };
|
|
41
|
+
}
|
|
42
|
+
const retained = new Set();
|
|
43
|
+
const retainNewest = (records) => {
|
|
44
|
+
for (let index = records.length - 1; index >= 0; index -= 1) {
|
|
45
|
+
if (retained.size >= MAX_EVIDENCE_LEDGER_RECORDS)
|
|
46
|
+
return;
|
|
47
|
+
retained.add(records[index]);
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
// Negative evidence for the current objective epoch is safety-critical. Keep
|
|
51
|
+
// it ahead of passing and old-epoch records; if more than the entire bounded
|
|
52
|
+
// ledger is negative, the truncation summary below makes completion fail
|
|
53
|
+
// closed even after later passing evidence arrives.
|
|
54
|
+
retainNewest(all.filter(record => record.objectiveRevision === objectiveRevision && record.result !== 'passed'));
|
|
55
|
+
retainNewest(all.filter(record => record.objectiveRevision === objectiveRevision));
|
|
56
|
+
retainNewest(all);
|
|
57
|
+
const ledger = all.filter(record => retained.has(record));
|
|
58
|
+
const dropped = all.filter(record => record.objectiveRevision === objectiveRevision && !retained.has(record));
|
|
59
|
+
if (dropped.length === 0)
|
|
60
|
+
return { ledger, truncation: carried };
|
|
61
|
+
const increment = (result) => dropped.filter(record => record.result === result).length;
|
|
62
|
+
return {
|
|
63
|
+
ledger,
|
|
64
|
+
truncation: {
|
|
65
|
+
objectiveRevision,
|
|
66
|
+
droppedPassed: boundedTruncationCount(carried?.droppedPassed ?? 0, increment('passed')),
|
|
67
|
+
droppedFailed: boundedTruncationCount(carried?.droppedFailed ?? 0, increment('failed')),
|
|
68
|
+
droppedInconclusive: boundedTruncationCount(carried?.droppedInconclusive ?? 0, increment('inconclusive')),
|
|
69
|
+
},
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
function finalEvidenceReceipt(record) {
|
|
73
|
+
return {
|
|
74
|
+
evidenceId: record.id,
|
|
75
|
+
kind: record.kind,
|
|
76
|
+
provenance: record.kind === 'user'
|
|
77
|
+
? 'user_acceptance'
|
|
78
|
+
: record.kind === 'external'
|
|
79
|
+
? 'external'
|
|
80
|
+
: 'runtime_automatic',
|
|
81
|
+
result: record.result,
|
|
82
|
+
subject: record.subject,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
function noProgressTurnSummary(outcome) {
|
|
86
|
+
const evidence = outcome.evidenceRecords ?? [];
|
|
87
|
+
return {
|
|
88
|
+
turnId: (0, redaction_1.redactTraceText)(String(outcome.turnId)).slice(0, 80),
|
|
89
|
+
endedAt: outcome.endedAt,
|
|
90
|
+
finishReason: (0, redaction_1.redactTraceText)(outcome.finishReason).slice(0, 80),
|
|
91
|
+
passedEvidence: evidence.filter(record => record.result === 'passed').length,
|
|
92
|
+
failedEvidence: evidence.filter(record => record.result === 'failed').length,
|
|
93
|
+
inconclusiveEvidence: evidence.filter(record => record.result === 'inconclusive').length,
|
|
94
|
+
planUpdateProposed: Boolean(outcome.pendingPlanUpdate),
|
|
95
|
+
blockerCategory: outcome.blocker?.category,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
function formatNoProgressTurns(turns) {
|
|
99
|
+
return turns
|
|
100
|
+
.map(turn => `${turn.turnId} finish=${turn.finishReason} evidence=${turn.passedEvidence}p/${turn.failedEvidence}f/${turn.inconclusiveEvidence}i plan=${turn.planUpdateProposed ? 'no-change' : 'none'}${turn.blockerCategory ? ` blocker=${turn.blockerCategory}` : ''}`)
|
|
101
|
+
.join('; ');
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Build a minimal pending contract from an objective string. Used at creation
|
|
105
|
+
* and at load time to normalize v0.1.1 sidecars (which have no contract).
|
|
106
|
+
*
|
|
107
|
+
* The primary criterion is `source=user` because it is the user's verbatim
|
|
108
|
+
* objective, with a stable id so repeated
|
|
109
|
+
* normalizations of the same goal produce the same id (no churn on reload).
|
|
110
|
+
* User-supplied criteria (`source=user`) are added via a separate path.
|
|
111
|
+
*/
|
|
112
|
+
function minimalContract(objective, now) {
|
|
113
|
+
return {
|
|
114
|
+
originalObjective: objective,
|
|
115
|
+
objectiveRevision: 0,
|
|
116
|
+
constraints: [],
|
|
117
|
+
successCriteria: [
|
|
118
|
+
{
|
|
119
|
+
id: 'criterion:primary',
|
|
120
|
+
statement: objective,
|
|
121
|
+
source: 'user',
|
|
122
|
+
status: 'pending',
|
|
123
|
+
// External completion actions cannot be proven by local process output.
|
|
124
|
+
// Other primary criteria accept local runtime evidence; model natural
|
|
125
|
+
// language is never an accepted kind.
|
|
126
|
+
requiredEvidenceKinds: (0, completion_audit_1.criterionRequiresExternalCompletionEvidence)(objective)
|
|
127
|
+
? ['external', 'user']
|
|
128
|
+
: ['test', 'build', 'file', 'runtime'],
|
|
129
|
+
evidenceRefs: [],
|
|
130
|
+
},
|
|
131
|
+
],
|
|
132
|
+
planSnapshot: {
|
|
133
|
+
revision: 0,
|
|
134
|
+
phase: 'initial',
|
|
135
|
+
steps: [],
|
|
136
|
+
updatedAt: now,
|
|
137
|
+
},
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
const CONTRACT_EVIDENCE_KINDS = new Set([
|
|
141
|
+
'test',
|
|
142
|
+
'build',
|
|
143
|
+
'lint',
|
|
144
|
+
'file',
|
|
145
|
+
'runtime',
|
|
146
|
+
'external',
|
|
147
|
+
'user',
|
|
148
|
+
]);
|
|
149
|
+
const BOUNDARY_CONFIRMATION_PATTERN = /\b(publish|deploy|delete|erase|remove|drop|migrate|send|email|message|post|pay|purchase|merge|push|tag|overwrite|destroy|revoke|transfer|upload)\b|发布|部署|删除|清空|迁移|发送|付款|购买|合并|推送|打标签|覆盖|销毁|撤销|转账|上传/i;
|
|
150
|
+
/** High-impact external or destructive goals must not auto-start from model-created text. */
|
|
151
|
+
function goalRequiresBoundaryConfirmation(objective, input = {}) {
|
|
152
|
+
return [
|
|
153
|
+
objective,
|
|
154
|
+
...(input.constraints ?? []),
|
|
155
|
+
...(input.successCriteria ?? []).map(criterion => criterion.statement),
|
|
156
|
+
].some(statement => (0, completion_audit_1.hasUnnegatedActionMatch)(statement, BOUNDARY_CONFIRMATION_PATTERN));
|
|
157
|
+
}
|
|
158
|
+
function boundaryConfirmationState(objectiveRevision, now) {
|
|
159
|
+
return {
|
|
160
|
+
status: 'paused',
|
|
161
|
+
stopReason: {
|
|
162
|
+
kind: 'user',
|
|
163
|
+
message: 'Boundary confirmation required for an external, destructive, or high-impact Goal. Review the objective and constraints, then use /target resume to confirm.',
|
|
164
|
+
at: now,
|
|
165
|
+
},
|
|
166
|
+
boundaryConfirmation: {
|
|
167
|
+
requiredAt: now,
|
|
168
|
+
reason: 'external_destructive_or_high_impact',
|
|
169
|
+
objectiveRevision,
|
|
170
|
+
},
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
function pauseForBoundaryConfirmation(goal, now) {
|
|
174
|
+
return {
|
|
175
|
+
...goal,
|
|
176
|
+
...boundaryConfirmationState(goal.contract?.objectiveRevision ?? 0, now),
|
|
177
|
+
updatedAt: now,
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
function creationContract(objective, now, input) {
|
|
181
|
+
const constraints = input.constraints ?? [];
|
|
182
|
+
const successCriteria = input.successCriteria ?? [];
|
|
183
|
+
if (constraints.length > 50 || successCriteria.length > 50)
|
|
184
|
+
return null;
|
|
185
|
+
if (constraints.some(statement => !statement.trim()) ||
|
|
186
|
+
successCriteria.some(criterion => !criterion.statement.trim() ||
|
|
187
|
+
criterion.requiredEvidenceKinds.length === 0 ||
|
|
188
|
+
criterion.requiredEvidenceKinds.some(kind => !CONTRACT_EVIDENCE_KINDS.has(kind)))) {
|
|
189
|
+
return null;
|
|
190
|
+
}
|
|
191
|
+
const contract = minimalContract(objective, now);
|
|
192
|
+
contract.constraints = constraints.map((statement, index) => ({
|
|
193
|
+
id: `constraint:user:${index + 1}`,
|
|
194
|
+
statement: statement.trim(),
|
|
195
|
+
source: 'user',
|
|
196
|
+
}));
|
|
197
|
+
contract.successCriteria.push(...successCriteria.map((criterion, index) => ({
|
|
198
|
+
id: `criterion:user:${index + 1}`,
|
|
199
|
+
statement: criterion.statement.trim(),
|
|
200
|
+
source: 'user',
|
|
201
|
+
status: 'pending',
|
|
202
|
+
requiredEvidenceKinds: [...new Set(criterion.requiredEvidenceKinds)],
|
|
203
|
+
evidenceRefs: [],
|
|
204
|
+
})));
|
|
205
|
+
return contract;
|
|
206
|
+
}
|
|
207
|
+
function compactPlanValue(value) {
|
|
208
|
+
const compact = value?.replace(/\s+/gu, ' ').trim();
|
|
209
|
+
return compact || undefined;
|
|
210
|
+
}
|
|
211
|
+
function stablePlanStepKey(value) {
|
|
212
|
+
return value
|
|
213
|
+
.normalize('NFKC')
|
|
214
|
+
.toLowerCase()
|
|
215
|
+
.replace(/[^\p{L}\p{N}]+/gu, ' ')
|
|
216
|
+
.trim()
|
|
217
|
+
.replace(/\s+/gu, ' ');
|
|
218
|
+
}
|
|
219
|
+
const OBJECTIVE_TRACE_STOP_WORDS = new Set([
|
|
220
|
+
'add',
|
|
221
|
+
'build',
|
|
222
|
+
'check',
|
|
223
|
+
'code',
|
|
224
|
+
'complete',
|
|
225
|
+
'create',
|
|
226
|
+
'goal',
|
|
227
|
+
'implement',
|
|
228
|
+
'pass',
|
|
229
|
+
'run',
|
|
230
|
+
'test',
|
|
231
|
+
'update',
|
|
232
|
+
'verify',
|
|
233
|
+
]);
|
|
234
|
+
function objectiveTraceTokens(value) {
|
|
235
|
+
const tokens = new Set();
|
|
236
|
+
for (const raw of value
|
|
237
|
+
.normalize('NFKC')
|
|
238
|
+
.toLowerCase()
|
|
239
|
+
.match(/[\p{L}\p{N}_-]+/gu) ?? []) {
|
|
240
|
+
if (raw.length >= 3 && !OBJECTIVE_TRACE_STOP_WORDS.has(raw))
|
|
241
|
+
tokens.add(raw);
|
|
242
|
+
if (/\p{Script=Han}/u.test(raw)) {
|
|
243
|
+
const characters = Array.from(raw);
|
|
244
|
+
for (let index = 0; index < characters.length - 1; index += 1) {
|
|
245
|
+
tokens.add(`${characters[index]}${characters[index + 1]}`);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
return tokens;
|
|
250
|
+
}
|
|
251
|
+
function criterionTracesToObjective(statement, contract) {
|
|
252
|
+
const objectiveTokens = objectiveTraceTokens(contract.originalObjective);
|
|
253
|
+
if (objectiveTokens.size === 0)
|
|
254
|
+
return false;
|
|
255
|
+
const criterionTokens = objectiveTraceTokens(statement);
|
|
256
|
+
return [...criterionTokens].some(token => objectiveTokens.has(token));
|
|
257
|
+
}
|
|
258
|
+
function substantivePlanProgress(contract, update, derivedCriteria) {
|
|
259
|
+
const current = contract.planSnapshot;
|
|
260
|
+
const completedStableStep = Boolean(current?.steps.some(step => {
|
|
261
|
+
if (step.done)
|
|
262
|
+
return false;
|
|
263
|
+
const key = stablePlanStepKey(step.description);
|
|
264
|
+
return Boolean(key && update.steps.some(next => next.done && stablePlanStepKey(next.description) === key));
|
|
265
|
+
}));
|
|
266
|
+
const traceableCriterionAdded = derivedCriteria.some(criterion => criterionTracesToObjective(criterion.statement, contract));
|
|
267
|
+
return completedStableStep || traceableCriterionAdded;
|
|
268
|
+
}
|
|
269
|
+
function stableEvidenceSource(sourceRef) {
|
|
270
|
+
const tool = /^tool:[^:]+:(.+)$/iu.exec(sourceRef.trim());
|
|
271
|
+
return compactPlanValue(tool ? `tool:${tool[1]}` : sourceRef)?.toLowerCase() ?? 'unknown';
|
|
272
|
+
}
|
|
273
|
+
function normalizedEvidenceSubject(subject) {
|
|
274
|
+
return (0, redaction_1.redactTraceText)(subject)
|
|
275
|
+
.normalize('NFKC')
|
|
276
|
+
.toLowerCase()
|
|
277
|
+
.replace(/[^\p{L}\p{N}_-]+/gu, ' ')
|
|
278
|
+
.trim()
|
|
279
|
+
.replace(/\s+/gu, ' ');
|
|
280
|
+
}
|
|
281
|
+
function evidenceProgressKeys(incoming, contract) {
|
|
282
|
+
const keys = new Set();
|
|
283
|
+
for (const record of incoming) {
|
|
284
|
+
if (record.result !== 'passed')
|
|
285
|
+
continue;
|
|
286
|
+
const stableSource = stableEvidenceSource(record.sourceRef);
|
|
287
|
+
const normalizedSubject = normalizedEvidenceSubject(record.subject);
|
|
288
|
+
const evidenceTokens = objectiveTraceTokens(`${normalizedSubject} ${stableSource}`);
|
|
289
|
+
for (const criterion of contract.successCriteria) {
|
|
290
|
+
if (!criterion.requiredEvidenceKinds.includes(record.kind))
|
|
291
|
+
continue;
|
|
292
|
+
const directCriterionReference = normalizedSubject.includes(criterion.id.toLowerCase()) ||
|
|
293
|
+
stableSource.includes(criterion.id.toLowerCase());
|
|
294
|
+
const criterionTokens = objectiveTraceTokens(criterion.statement);
|
|
295
|
+
const semanticallyRelated = [...criterionTokens].some(token => evidenceTokens.has(token));
|
|
296
|
+
if (!directCriterionReference && !semanticallyRelated)
|
|
297
|
+
continue;
|
|
298
|
+
const materialIdentity = [
|
|
299
|
+
record.objectiveRevision,
|
|
300
|
+
criterion.id,
|
|
301
|
+
record.kind,
|
|
302
|
+
stableSource,
|
|
303
|
+
normalizedSubject,
|
|
304
|
+
record.workspaceFingerprint ?? 'workspace:none',
|
|
305
|
+
].join('\u0000');
|
|
306
|
+
keys.add((0, crypto_1.createHash)('sha256').update(materialIdentity).digest('hex'));
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
return [...keys];
|
|
310
|
+
}
|
|
311
|
+
function criterionStateAdvanced(before, after, evidenceLedger) {
|
|
312
|
+
if (!after)
|
|
313
|
+
return false;
|
|
314
|
+
const passedEvidence = new Set(evidenceLedger.filter(record => record.result === 'passed').map(record => record.id));
|
|
315
|
+
const priorById = new Map(before.successCriteria.map(criterion => [criterion.id, criterion]));
|
|
316
|
+
return after.successCriteria.some(criterion => {
|
|
317
|
+
const prior = priorById.get(criterion.id);
|
|
318
|
+
if (!prior)
|
|
319
|
+
return false;
|
|
320
|
+
if (prior.status !== 'passed' && criterion.status === 'passed')
|
|
321
|
+
return true;
|
|
322
|
+
const priorRefs = new Set(prior.evidenceRefs);
|
|
323
|
+
return criterion.evidenceRefs.some(ref => !priorRefs.has(ref) && passedEvidence.has(ref));
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
function planContentChanged(contract, update) {
|
|
327
|
+
const current = contract.planSnapshot;
|
|
328
|
+
if (!current)
|
|
329
|
+
return true;
|
|
330
|
+
if (compactPlanValue(current.phase) !== compactPlanValue(update.phase))
|
|
331
|
+
return true;
|
|
332
|
+
if (compactPlanValue(current.nextAction) !== compactPlanValue(update.nextAction))
|
|
333
|
+
return true;
|
|
334
|
+
if (current.steps.length !== update.steps.length)
|
|
335
|
+
return true;
|
|
336
|
+
return current.steps.some((step, index) => {
|
|
337
|
+
const next = update.steps[index];
|
|
338
|
+
return (compactPlanValue(step.description) !== compactPlanValue(next.description) ||
|
|
339
|
+
step.done !== next.done);
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
function newDerivedCriteria(contract, update, planRevision) {
|
|
343
|
+
const existingStatements = new Set(contract.successCriteria.map(criterion => criterion.statement.trim().toLowerCase()));
|
|
344
|
+
return update.derivedCriteria
|
|
345
|
+
.filter(item => !existingStatements.has(item.statement.trim().toLowerCase()))
|
|
346
|
+
.map((item, index) => ({
|
|
347
|
+
id: `criterion:derived:${planRevision}:${index + 1}`,
|
|
348
|
+
statement: item.statement,
|
|
349
|
+
source: 'derived',
|
|
350
|
+
status: 'pending',
|
|
351
|
+
requiredEvidenceKinds: item.requiredEvidenceKinds,
|
|
352
|
+
evidenceRefs: [],
|
|
353
|
+
}));
|
|
354
|
+
}
|
|
355
|
+
/** Ensure a loaded goal carries a contract; normalize v0.1.1 sidecars. */
|
|
356
|
+
function ensureContract(goal) {
|
|
357
|
+
if (goal.contract)
|
|
358
|
+
return goal;
|
|
359
|
+
const contract = minimalContract(goal.objective, goal.updatedAt || goal.createdAt);
|
|
360
|
+
const normalizedLegacy = {
|
|
361
|
+
...goal,
|
|
362
|
+
contract,
|
|
363
|
+
completedAt: undefined,
|
|
364
|
+
completionAudit: undefined,
|
|
365
|
+
};
|
|
366
|
+
if (goal.status === 'active') {
|
|
367
|
+
return {
|
|
368
|
+
...normalizedLegacy,
|
|
369
|
+
blocker: undefined,
|
|
370
|
+
stopReason: undefined,
|
|
371
|
+
};
|
|
372
|
+
}
|
|
373
|
+
if (goal.status === 'blocked' && (!goal.blocker || goal.stopReason?.kind !== 'blocked')) {
|
|
374
|
+
return {
|
|
375
|
+
...normalizedLegacy,
|
|
376
|
+
status: 'paused',
|
|
377
|
+
blocker: undefined,
|
|
378
|
+
stopReason: {
|
|
379
|
+
kind: 'user',
|
|
380
|
+
message: 'Legacy blocked state requires v0.1.2 blocker re-verification.',
|
|
381
|
+
at: Date.now(),
|
|
382
|
+
},
|
|
383
|
+
};
|
|
384
|
+
}
|
|
385
|
+
if (goal.status !== 'complete')
|
|
386
|
+
return normalizedLegacy;
|
|
387
|
+
const now = Date.now();
|
|
388
|
+
return {
|
|
389
|
+
...normalizedLegacy,
|
|
390
|
+
status: 'paused',
|
|
391
|
+
stopReason: {
|
|
392
|
+
kind: 'user',
|
|
393
|
+
message: 'Legacy completion requires v0.1.2 evidence re-verification.',
|
|
394
|
+
at: now,
|
|
395
|
+
},
|
|
396
|
+
};
|
|
397
|
+
}
|
|
15
398
|
class GoalCoordinator {
|
|
16
399
|
constructor(projectPath, sessionId) {
|
|
17
400
|
this.projectPath = projectPath;
|
|
18
401
|
this.sessionId = sessionId;
|
|
19
402
|
this.state = { goal: null, generation: 0, continuationDeferred: false };
|
|
403
|
+
this.loadIssue = null;
|
|
404
|
+
/** Runtime-only dedupe for interrupted stale turns; in-flight turns do not survive restart. */
|
|
405
|
+
this.accountedStaleTurnKeys = new Set();
|
|
406
|
+
/** Last disk-authoritative state, used when both a write and recovery read fail. */
|
|
407
|
+
this.persistedGoalSnapshot = null;
|
|
20
408
|
}
|
|
21
409
|
// ==========================================================================
|
|
22
410
|
// Queries
|
|
23
411
|
// ==========================================================================
|
|
24
|
-
get goal() {
|
|
25
|
-
|
|
26
|
-
|
|
412
|
+
get goal() {
|
|
413
|
+
return this.state.goal;
|
|
414
|
+
}
|
|
415
|
+
get generation() {
|
|
416
|
+
return this.state.generation;
|
|
417
|
+
}
|
|
418
|
+
get boundSessionId() {
|
|
419
|
+
return this.sessionId;
|
|
420
|
+
}
|
|
421
|
+
get isActive() {
|
|
422
|
+
return this.state.goal?.status === 'active';
|
|
423
|
+
}
|
|
27
424
|
get canContinue() {
|
|
28
|
-
return this.isActive
|
|
29
|
-
|
|
30
|
-
|
|
425
|
+
return (this.isActive &&
|
|
426
|
+
!this.state.continuationDeferred &&
|
|
427
|
+
!(0, accounting_1.isBudgetExceeded)(this.state.goal.tokensUsed, this.state.goal.tokenBudget));
|
|
428
|
+
}
|
|
429
|
+
get lastLoadIssue() {
|
|
430
|
+
return this.loadIssue;
|
|
431
|
+
}
|
|
432
|
+
addGoalInteger(value, increment, field) {
|
|
433
|
+
try {
|
|
434
|
+
return addSafeInteger(value, increment, field);
|
|
435
|
+
}
|
|
436
|
+
catch (error) {
|
|
437
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
438
|
+
this.failClosedAfterPersistenceError(message);
|
|
439
|
+
throw error;
|
|
440
|
+
}
|
|
31
441
|
}
|
|
32
442
|
snapshot() {
|
|
33
443
|
const g = this.state.goal;
|
|
34
444
|
if (!g)
|
|
35
445
|
return null;
|
|
446
|
+
const criteria = g.contract?.successCriteria ?? [];
|
|
36
447
|
return {
|
|
37
448
|
goalId: g.goalId,
|
|
38
449
|
revision: g.revision,
|
|
@@ -44,37 +455,115 @@ class GoalCoordinator {
|
|
|
44
455
|
continuationCount: g.continuationCount,
|
|
45
456
|
updatedAt: g.updatedAt,
|
|
46
457
|
stopReason: g.stopReason?.message,
|
|
458
|
+
criteria: {
|
|
459
|
+
passed: criteria.filter(criterion => criterion.status === 'passed').length,
|
|
460
|
+
failed: criteria.filter(criterion => criterion.status === 'failed').length,
|
|
461
|
+
stale: criteria.filter(criterion => criterion.status === 'stale').length,
|
|
462
|
+
total: criteria.length,
|
|
463
|
+
},
|
|
464
|
+
planRevision: g.contract?.planSnapshot?.revision,
|
|
465
|
+
planPhase: g.contract?.planSnapshot?.phase,
|
|
466
|
+
nextAction: g.contract?.planSnapshot?.nextAction,
|
|
467
|
+
auditRemaining: g.completionAudit?.remainingRequirements,
|
|
47
468
|
};
|
|
48
469
|
}
|
|
49
470
|
// ==========================================================================
|
|
50
471
|
// Lifecycle
|
|
51
472
|
// ==========================================================================
|
|
52
|
-
load() {
|
|
473
|
+
load(recoverActive = false) {
|
|
53
474
|
const result = (0, goal_storage_1.loadGoal)(this.projectPath, this.sessionId);
|
|
54
475
|
if (result.ok) {
|
|
55
|
-
this.
|
|
56
|
-
|
|
476
|
+
this.loadIssue = null;
|
|
477
|
+
// v0.1.2: normalize v0.1.1 sidecars (no contract) into a minimal pending
|
|
478
|
+
// contract. This is in-memory only; it does not rewrite the sidecar
|
|
479
|
+
// until the next real state update.
|
|
480
|
+
this.state.goal = ensureContract(result.value);
|
|
481
|
+
this.persistedGoalSnapshot = structuredClone(this.state.goal);
|
|
482
|
+
// Every reload replaces the in-memory authority, even if the disk bytes
|
|
483
|
+
// are unchanged. Advance the runtime epoch so pre-reload outcomes cannot
|
|
484
|
+
// finalize or account against the newly loaded state.
|
|
485
|
+
this.state.generation += 1;
|
|
57
486
|
this.state.continuationDeferred = false;
|
|
487
|
+
if (recoverActive && this.state.goal.status === 'active') {
|
|
488
|
+
const loadedGoal = this.state.goal;
|
|
489
|
+
const lockPath = (0, path_1.join)((0, config_dir_1.getProjectSessionsDir)(this.projectPath), `${this.sessionId}.goal.json.lock`);
|
|
490
|
+
if ((0, fs_1.existsSync)(lockPath)) {
|
|
491
|
+
const message = `Restart recovery could not persist a paused Goal because storage is temporarily locked at ${lockPath}. No continuation was started; resolve the lock, then use /target resume.`;
|
|
492
|
+
this.loadIssue = { code: 'io_error', message };
|
|
493
|
+
this.failClosedAfterPersistenceError(message);
|
|
494
|
+
return true;
|
|
495
|
+
}
|
|
496
|
+
try {
|
|
497
|
+
this.state.goal = {
|
|
498
|
+
...loadedGoal,
|
|
499
|
+
status: 'paused',
|
|
500
|
+
revision: addSafeInteger(loadedGoal.revision, 1, 'revision'),
|
|
501
|
+
updatedAt: Date.now(),
|
|
502
|
+
stopReason: {
|
|
503
|
+
kind: 'user',
|
|
504
|
+
message: 'Recovered after restart. Use /target resume to continue.',
|
|
505
|
+
at: Date.now(),
|
|
506
|
+
},
|
|
507
|
+
};
|
|
508
|
+
this.state.continuationDeferred = true;
|
|
509
|
+
this.persist();
|
|
510
|
+
}
|
|
511
|
+
catch (error) {
|
|
512
|
+
const detail = (0, redaction_1.redactTraceText)(error instanceof Error ? error.message : String(error));
|
|
513
|
+
const message = `Restart recovery could not persist a paused Goal. No continuation was started; resolve the storage error, then use /target resume. ${detail}`;
|
|
514
|
+
this.loadIssue = { code: 'io_error', message };
|
|
515
|
+
this.failClosedAfterPersistenceError(message);
|
|
516
|
+
}
|
|
517
|
+
}
|
|
58
518
|
return true;
|
|
59
519
|
}
|
|
520
|
+
this.loadIssue = ['corrupt', 'metadata_mismatch', 'incompatible_schema', 'io_error'].includes(result.error)
|
|
521
|
+
? {
|
|
522
|
+
code: result.error,
|
|
523
|
+
message: result.message,
|
|
524
|
+
}
|
|
525
|
+
: null;
|
|
526
|
+
// A failed repeat load makes the disk state authoritative and the previous
|
|
527
|
+
// in-memory Goal unsafe to continue. Invalidate in-flight work and discard
|
|
528
|
+
// both cached copies without attempting to write the stale Goal back.
|
|
529
|
+
this.state.generation += 1;
|
|
530
|
+
this.state.continuationDeferred = true;
|
|
531
|
+
this.state.goal = null;
|
|
532
|
+
this.persistedGoalSnapshot = null;
|
|
60
533
|
return false;
|
|
61
534
|
}
|
|
62
|
-
create(objective) {
|
|
535
|
+
create(objective, contractInput = {}) {
|
|
63
536
|
if (this.state.goal && !types_1.GOAL_TERMINAL_STATES.has(this.state.goal.status)) {
|
|
64
|
-
return {
|
|
537
|
+
return {
|
|
538
|
+
ok: false,
|
|
539
|
+
error: 'An active goal already exists. Use /target replace to change it.',
|
|
540
|
+
};
|
|
65
541
|
}
|
|
66
542
|
if (!objective.trim()) {
|
|
67
543
|
return { ok: false, error: 'Objective cannot be empty.' };
|
|
68
544
|
}
|
|
69
545
|
if (objective.length > types_1.GOAL_INVARIANTS.maxObjectiveChars) {
|
|
70
|
-
return {
|
|
546
|
+
return {
|
|
547
|
+
ok: false,
|
|
548
|
+
error: `Objective too long (max ${types_1.GOAL_INVARIANTS.maxObjectiveChars} chars).`,
|
|
549
|
+
};
|
|
550
|
+
}
|
|
551
|
+
const now = Date.now();
|
|
552
|
+
const contract = creationContract(objective.trim(), now, contractInput);
|
|
553
|
+
if (!contract) {
|
|
554
|
+
return { ok: false, error: 'Goal constraints or success criteria are invalid.' };
|
|
71
555
|
}
|
|
72
|
-
const
|
|
556
|
+
const requiresBoundaryConfirmation = goalRequiresBoundaryConfirmation(objective, contractInput);
|
|
557
|
+
const result = (0, goal_storage_1.createGoal)(this.projectPath, this.sessionId, objective, contract, this.state.goal?.revision, requiresBoundaryConfirmation
|
|
558
|
+
? boundaryConfirmationState(contract.objectiveRevision, now)
|
|
559
|
+
: undefined);
|
|
73
560
|
if (!result.ok)
|
|
74
561
|
return { ok: false, error: result.message };
|
|
75
|
-
|
|
562
|
+
const created = result.value;
|
|
563
|
+
this.state.goal = created;
|
|
564
|
+
this.persistedGoalSnapshot = structuredClone(created);
|
|
76
565
|
this.state.generation += 1;
|
|
77
|
-
this.state.continuationDeferred =
|
|
566
|
+
this.state.continuationDeferred = created.status !== 'active';
|
|
78
567
|
return { ok: true };
|
|
79
568
|
}
|
|
80
569
|
pause() {
|
|
@@ -83,7 +572,7 @@ class GoalCoordinator {
|
|
|
83
572
|
this.state.goal = {
|
|
84
573
|
...this.state.goal,
|
|
85
574
|
status: 'paused',
|
|
86
|
-
revision: this.state.goal.revision
|
|
575
|
+
revision: this.addGoalInteger(this.state.goal.revision, 1, 'revision'),
|
|
87
576
|
updatedAt: Date.now(),
|
|
88
577
|
stopReason: { kind: 'user', message: 'Paused by user.', at: Date.now() },
|
|
89
578
|
};
|
|
@@ -91,32 +580,169 @@ class GoalCoordinator {
|
|
|
91
580
|
this.state.continuationDeferred = true;
|
|
92
581
|
return true;
|
|
93
582
|
}
|
|
94
|
-
resume() {
|
|
583
|
+
resume(options = {}) {
|
|
95
584
|
if (!this.state.goal || !types_1.GOAL_USER_RECOVERABLE_STATES.has(this.state.goal.status))
|
|
96
585
|
return false;
|
|
586
|
+
const pendingBoundary = this.state.goal.boundaryConfirmation &&
|
|
587
|
+
this.state.goal.boundaryConfirmation.confirmedAt === undefined;
|
|
588
|
+
if (pendingBoundary &&
|
|
589
|
+
(!options.confirmBoundary ||
|
|
590
|
+
options.expectedGoalId !== this.state.goal.goalId ||
|
|
591
|
+
options.expectedRevision !== this.state.goal.revision)) {
|
|
592
|
+
return false;
|
|
593
|
+
}
|
|
594
|
+
const resumedAt = Date.now();
|
|
97
595
|
this.state.goal = {
|
|
98
596
|
...this.state.goal,
|
|
99
597
|
status: 'active',
|
|
100
|
-
revision: this.state.goal.revision
|
|
101
|
-
updatedAt:
|
|
102
|
-
activeSince:
|
|
598
|
+
revision: this.addGoalInteger(this.state.goal.revision, 1, 'revision'),
|
|
599
|
+
updatedAt: resumedAt,
|
|
600
|
+
activeSince: resumedAt,
|
|
103
601
|
stopReason: undefined,
|
|
104
602
|
blocker: undefined,
|
|
105
603
|
noProgressCount: 0,
|
|
604
|
+
recentNoProgressTurns: [],
|
|
605
|
+
boundaryConfirmation: pendingBoundary
|
|
606
|
+
? {
|
|
607
|
+
...this.state.goal.boundaryConfirmation,
|
|
608
|
+
confirmedAt: resumedAt,
|
|
609
|
+
confirmedRevision: this.state.goal.revision,
|
|
610
|
+
}
|
|
611
|
+
: this.state.goal.boundaryConfirmation,
|
|
106
612
|
};
|
|
107
613
|
this.persist();
|
|
108
614
|
this.state.continuationDeferred = false;
|
|
109
615
|
return true;
|
|
110
616
|
}
|
|
111
|
-
edit(objective) {
|
|
112
|
-
if (!this.state.goal)
|
|
617
|
+
edit(objective, reason = 'User invoked /target edit.') {
|
|
618
|
+
if (!this.state.goal || types_1.GOAL_TERMINAL_STATES.has(this.state.goal.status))
|
|
113
619
|
return false;
|
|
114
620
|
if (!objective.trim())
|
|
115
621
|
return false;
|
|
622
|
+
const g = this.state.goal;
|
|
623
|
+
const nextObjective = objective.trim();
|
|
624
|
+
if (nextObjective === g.objective)
|
|
625
|
+
return true;
|
|
626
|
+
const changedAt = Date.now();
|
|
627
|
+
// v0.1.2: preserve originalObjective; record an objective revision on the
|
|
628
|
+
// contract so steering can never silently rewrite the root goal. The
|
|
629
|
+
// top-level `objective` reflects the current wording.
|
|
630
|
+
const currentContract = g.contract ?? minimalContract(g.objective, g.updatedAt);
|
|
631
|
+
const objectiveRevision = this.addGoalInteger(currentContract.objectiveRevision, 1, 'contract.objectiveRevision');
|
|
632
|
+
const contract = {
|
|
633
|
+
...currentContract,
|
|
634
|
+
originalObjective: currentContract.originalObjective,
|
|
635
|
+
objectiveRevision,
|
|
636
|
+
objectiveHistory: [
|
|
637
|
+
...(currentContract.objectiveHistory ?? []),
|
|
638
|
+
{
|
|
639
|
+
revision: objectiveRevision,
|
|
640
|
+
previousObjective: g.objective,
|
|
641
|
+
objective: nextObjective,
|
|
642
|
+
reason: reason.trim() || 'User invoked /target edit.',
|
|
643
|
+
changedAt,
|
|
644
|
+
source: 'user',
|
|
645
|
+
},
|
|
646
|
+
].slice(-50),
|
|
647
|
+
successCriteria: currentContract.successCriteria.map(criterion => criterion.id === 'criterion:primary'
|
|
648
|
+
? {
|
|
649
|
+
...criterion,
|
|
650
|
+
statement: nextObjective,
|
|
651
|
+
source: 'user',
|
|
652
|
+
status: 'pending',
|
|
653
|
+
evidenceRefs: [],
|
|
654
|
+
}
|
|
655
|
+
: criterion),
|
|
656
|
+
};
|
|
657
|
+
const edited = {
|
|
658
|
+
...g,
|
|
659
|
+
objective: nextObjective,
|
|
660
|
+
contract,
|
|
661
|
+
revision: this.addGoalInteger(g.revision, 1, 'revision'),
|
|
662
|
+
updatedAt: changedAt,
|
|
663
|
+
completionAudit: undefined,
|
|
664
|
+
// Truncation is scoped to an objective epoch. A user edit intentionally
|
|
665
|
+
// starts a fresh verification epoch; old evidence remains provenance but
|
|
666
|
+
// cannot permanently block the edited objective.
|
|
667
|
+
evidenceLedgerTruncation: undefined,
|
|
668
|
+
};
|
|
669
|
+
this.state.goal = goalRequiresBoundaryConfirmation(nextObjective)
|
|
670
|
+
? { ...pauseForBoundaryConfirmation(edited, changedAt), revision: edited.revision }
|
|
671
|
+
: { ...edited, boundaryConfirmation: undefined };
|
|
672
|
+
this.persist();
|
|
673
|
+
this.state.continuationDeferred = this.state.goal.status !== 'active';
|
|
674
|
+
return true;
|
|
675
|
+
}
|
|
676
|
+
/** Record trusted human acceptance for a criterion that requires user evidence. */
|
|
677
|
+
confirmCriterion(criterionId) {
|
|
678
|
+
const goal = this.state.goal;
|
|
679
|
+
const contract = goal?.contract;
|
|
680
|
+
if (!goal || !contract || types_1.GOAL_TERMINAL_STATES.has(goal.status))
|
|
681
|
+
return false;
|
|
682
|
+
const criterion = contract.successCriteria.find(item => item.id === criterionId.trim());
|
|
683
|
+
if (!criterion || !criterion.requiredEvidenceKinds.includes('user'))
|
|
684
|
+
return false;
|
|
685
|
+
const now = Date.now();
|
|
686
|
+
const revision = this.addGoalInteger(goal.revision, 1, 'revision');
|
|
687
|
+
const evidenceId = `evidence:user:${(0, crypto_1.randomUUID)()}`;
|
|
688
|
+
const record = {
|
|
689
|
+
id: evidenceId,
|
|
690
|
+
goalId: goal.goalId,
|
|
691
|
+
goalRevision: revision,
|
|
692
|
+
objectiveRevision: contract.objectiveRevision,
|
|
693
|
+
turnId: `user-confirmation:${revision}`,
|
|
694
|
+
kind: 'user',
|
|
695
|
+
subject: `User confirmed ${criterion.id}: ${criterion.statement}`.slice(0, 240),
|
|
696
|
+
result: 'passed',
|
|
697
|
+
sourceRef: 'user:/target-confirm',
|
|
698
|
+
capturedAt: now,
|
|
699
|
+
redacted: true,
|
|
700
|
+
};
|
|
701
|
+
const boundedEvidence = appendBoundedEvidence(goal.evidenceLedger ?? [], [record], contract.objectiveRevision, goal.evidenceLedgerTruncation);
|
|
116
702
|
this.state.goal = {
|
|
117
|
-
...
|
|
118
|
-
|
|
119
|
-
|
|
703
|
+
...goal,
|
|
704
|
+
revision,
|
|
705
|
+
updatedAt: now,
|
|
706
|
+
evidenceLedger: boundedEvidence.ledger,
|
|
707
|
+
evidenceLedgerTruncation: boundedEvidence.truncation,
|
|
708
|
+
contract: {
|
|
709
|
+
...contract,
|
|
710
|
+
successCriteria: contract.successCriteria.map(item => item.id === criterion.id
|
|
711
|
+
? {
|
|
712
|
+
...item,
|
|
713
|
+
status: 'passed',
|
|
714
|
+
evidenceRefs: [...new Set([...item.evidenceRefs, evidenceId])],
|
|
715
|
+
}
|
|
716
|
+
: item),
|
|
717
|
+
},
|
|
718
|
+
completionAudit: undefined,
|
|
719
|
+
};
|
|
720
|
+
this.persist();
|
|
721
|
+
return true;
|
|
722
|
+
}
|
|
723
|
+
addConstraint(statement) {
|
|
724
|
+
const goal = this.state.goal;
|
|
725
|
+
const trimmed = statement.trim();
|
|
726
|
+
if (!goal || goal.status === 'complete' || !trimmed)
|
|
727
|
+
return false;
|
|
728
|
+
const contract = goal.contract ?? minimalContract(goal.objective, goal.updatedAt);
|
|
729
|
+
if (contract.constraints.some(constraint => constraint.statement === trimmed))
|
|
730
|
+
return true;
|
|
731
|
+
const revision = this.addGoalInteger(goal.revision, 1, 'revision');
|
|
732
|
+
this.state.goal = {
|
|
733
|
+
...goal,
|
|
734
|
+
contract: {
|
|
735
|
+
...contract,
|
|
736
|
+
constraints: [
|
|
737
|
+
...contract.constraints,
|
|
738
|
+
{
|
|
739
|
+
id: `constraint:user:${revision}`,
|
|
740
|
+
statement: trimmed,
|
|
741
|
+
source: 'user',
|
|
742
|
+
},
|
|
743
|
+
],
|
|
744
|
+
},
|
|
745
|
+
revision,
|
|
120
746
|
updatedAt: Date.now(),
|
|
121
747
|
};
|
|
122
748
|
this.persist();
|
|
@@ -125,22 +751,32 @@ class GoalCoordinator {
|
|
|
125
751
|
replace(objective) {
|
|
126
752
|
if (!objective.trim())
|
|
127
753
|
return false;
|
|
128
|
-
// Create new goal
|
|
129
|
-
|
|
130
|
-
|
|
754
|
+
// Create new goal with fresh goalId and fresh contract; old generation
|
|
755
|
+
// invalidated. Does not reuse the old goal's completion state.
|
|
756
|
+
const now = Date.now();
|
|
757
|
+
const contract = minimalContract(objective.trim(), now);
|
|
758
|
+
const requiresBoundaryConfirmation = goalRequiresBoundaryConfirmation(objective);
|
|
759
|
+
const result = (0, goal_storage_1.createGoal)(this.projectPath, this.sessionId, objective, contract, this.state.goal?.revision, requiresBoundaryConfirmation
|
|
760
|
+
? boundaryConfirmationState(contract.objectiveRevision, now)
|
|
761
|
+
: undefined);
|
|
762
|
+
if (!result.ok) {
|
|
763
|
+
this.restoreDiskAuthorityAfterMutationFailure();
|
|
131
764
|
return false;
|
|
132
|
-
|
|
765
|
+
}
|
|
766
|
+
const replacement = result.value;
|
|
767
|
+
this.state.goal = replacement;
|
|
768
|
+
this.persistedGoalSnapshot = structuredClone(replacement);
|
|
133
769
|
this.state.generation += 1;
|
|
134
|
-
this.state.continuationDeferred =
|
|
770
|
+
this.state.continuationDeferred = replacement.status !== 'active';
|
|
135
771
|
return true;
|
|
136
772
|
}
|
|
137
773
|
setBudget(tokenBudget) {
|
|
138
|
-
if (!this.state.goal)
|
|
774
|
+
if (!this.state.goal || this.state.goal.status === 'complete')
|
|
139
775
|
return false;
|
|
140
776
|
this.state.goal = {
|
|
141
777
|
...this.state.goal,
|
|
142
778
|
tokenBudget: tokenBudget ?? undefined,
|
|
143
|
-
revision: this.state.goal.revision
|
|
779
|
+
revision: this.addGoalInteger(this.state.goal.revision, 1, 'revision'),
|
|
144
780
|
updatedAt: Date.now(),
|
|
145
781
|
};
|
|
146
782
|
this.persist();
|
|
@@ -149,8 +785,13 @@ class GoalCoordinator {
|
|
|
149
785
|
clear() {
|
|
150
786
|
if (!this.state.goal)
|
|
151
787
|
return false;
|
|
152
|
-
(0, goal_storage_1.deleteGoal)(this.projectPath, this.sessionId);
|
|
788
|
+
const result = (0, goal_storage_1.deleteGoal)(this.projectPath, this.sessionId, this.state.goal.revision);
|
|
789
|
+
if (!result.ok) {
|
|
790
|
+
this.restoreDiskAuthorityAfterMutationFailure();
|
|
791
|
+
throw new Error(`${result.error}: ${result.message}`);
|
|
792
|
+
}
|
|
153
793
|
this.state.goal = null;
|
|
794
|
+
this.persistedGoalSnapshot = null;
|
|
154
795
|
this.state.generation += 1;
|
|
155
796
|
this.state.continuationDeferred = false;
|
|
156
797
|
return true;
|
|
@@ -162,15 +803,23 @@ class GoalCoordinator {
|
|
|
162
803
|
if (!this.canContinue)
|
|
163
804
|
return null;
|
|
164
805
|
const g = this.state.goal;
|
|
806
|
+
let continuationIndex;
|
|
807
|
+
try {
|
|
808
|
+
continuationIndex = this.addGoalInteger(g.continuationCount, 1, 'continuationCount');
|
|
809
|
+
}
|
|
810
|
+
catch {
|
|
811
|
+
return null;
|
|
812
|
+
}
|
|
165
813
|
return {
|
|
166
814
|
inputKind: 'goal_continuation',
|
|
167
815
|
sessionId: this.sessionId,
|
|
168
816
|
persistAsUserMessage: false,
|
|
169
817
|
echoToTranscript: false,
|
|
818
|
+
generation: this.state.generation,
|
|
170
819
|
goal: {
|
|
171
820
|
goalId: g.goalId,
|
|
172
821
|
revision: g.revision,
|
|
173
|
-
continuationIndex
|
|
822
|
+
continuationIndex,
|
|
174
823
|
},
|
|
175
824
|
};
|
|
176
825
|
}
|
|
@@ -178,17 +827,115 @@ class GoalCoordinator {
|
|
|
178
827
|
const g = this.state.goal;
|
|
179
828
|
if (!g)
|
|
180
829
|
return;
|
|
181
|
-
if (outcome.
|
|
830
|
+
if (outcome.sessionId !== this.sessionId ||
|
|
831
|
+
outcome.goalId !== g.goalId ||
|
|
832
|
+
outcome.goalRevision !== g.revision ||
|
|
833
|
+
(outcome.goalGeneration !== undefined && outcome.goalGeneration !== this.state.generation))
|
|
182
834
|
return;
|
|
183
|
-
|
|
184
|
-
|
|
835
|
+
if (outcome.usageComplete === false) {
|
|
836
|
+
const accounting = (0, accounting_1.accumulateTurn)({
|
|
837
|
+
tokensUsed: g.tokensUsed,
|
|
838
|
+
timeUsedMs: g.timeUsedMs,
|
|
839
|
+
continuationCount: g.continuationCount,
|
|
840
|
+
noProgressCount: g.noProgressCount,
|
|
841
|
+
}, outcome);
|
|
842
|
+
const pausedAt = Date.now();
|
|
843
|
+
const tokensUsed = this.addGoalInteger(accounting.tokensUsed, 0, 'tokensUsed');
|
|
844
|
+
const timeUsedMs = this.addGoalInteger(accounting.timeUsedMs, 0, 'timeUsedMs');
|
|
845
|
+
const continuationCount = this.addGoalInteger(accounting.continuationCount, 0, 'continuationCount');
|
|
846
|
+
const noProgressCount = this.addGoalInteger(accounting.noProgressCount, 0, 'noProgressCount');
|
|
847
|
+
this.state.goal = {
|
|
848
|
+
...g,
|
|
849
|
+
tokensUsed,
|
|
850
|
+
timeUsedMs,
|
|
851
|
+
continuationCount,
|
|
852
|
+
noProgressCount,
|
|
853
|
+
status: 'paused',
|
|
854
|
+
revision: this.addGoalInteger(g.revision, 1, 'revision'),
|
|
855
|
+
updatedAt: pausedAt,
|
|
856
|
+
lastTurn: {
|
|
857
|
+
turnId: outcome.turnId,
|
|
858
|
+
finishReason: outcome.finishReason,
|
|
859
|
+
endedAt: outcome.endedAt,
|
|
860
|
+
promptTokens: outcome.usage.promptTokens,
|
|
861
|
+
completionTokens: outcome.usage.completionTokens,
|
|
862
|
+
subagentTokens: outcome.usage.subagentTokens,
|
|
863
|
+
totalTokens: outcome.usage.totalTokens,
|
|
864
|
+
madeProgress: outcome.madeProgress,
|
|
865
|
+
},
|
|
866
|
+
stopReason: {
|
|
867
|
+
kind: 'runtime_error',
|
|
868
|
+
message: 'Paused because provider usage was missing or incomplete across retry/fallback attempts; known usage was recorded, and budget accounting is incomplete.',
|
|
869
|
+
at: pausedAt,
|
|
870
|
+
},
|
|
871
|
+
};
|
|
872
|
+
this.state.continuationDeferred = true;
|
|
873
|
+
this.persist();
|
|
874
|
+
return;
|
|
875
|
+
}
|
|
876
|
+
const currentContract = g.contract ?? minimalContract(g.objective, g.updatedAt);
|
|
877
|
+
const pendingPlanRevision = outcome.pendingPlanUpdate
|
|
878
|
+
? this.addGoalInteger(currentContract.planSnapshot?.revision ?? 0, 1, 'contract.planSnapshot.revision')
|
|
879
|
+
: 0;
|
|
880
|
+
const pendingDerivedCriteria = outcome.pendingPlanUpdate
|
|
881
|
+
? newDerivedCriteria(currentContract, outcome.pendingPlanUpdate, pendingPlanRevision)
|
|
882
|
+
: [];
|
|
883
|
+
const pendingPlanChangesContent = outcome.pendingPlanUpdate
|
|
884
|
+
? planContentChanged(currentContract, outcome.pendingPlanUpdate)
|
|
885
|
+
: false;
|
|
886
|
+
const pendingPlanMutatesContract = Boolean(outcome.pendingPlanUpdate && (pendingPlanChangesContent || pendingDerivedCriteria.length > 0));
|
|
887
|
+
const newEvidence = (outcome.evidenceRecords ?? []).filter(record => record.goalId === g.goalId &&
|
|
888
|
+
record.goalRevision === g.revision &&
|
|
889
|
+
record.objectiveRevision === (g.contract?.objectiveRevision ?? 0) &&
|
|
890
|
+
record.turnId === outcome.turnId);
|
|
891
|
+
const planAdvanced = Boolean(outcome.pendingPlanUpdate &&
|
|
892
|
+
substantivePlanProgress(currentContract, outcome.pendingPlanUpdate, pendingDerivedCriteria));
|
|
893
|
+
const evidenceContract = pendingDerivedCriteria.length
|
|
894
|
+
? {
|
|
895
|
+
...currentContract,
|
|
896
|
+
successCriteria: [...currentContract.successCriteria, ...pendingDerivedCriteria],
|
|
897
|
+
}
|
|
898
|
+
: currentContract;
|
|
899
|
+
const incomingEvidenceKeys = evidenceProgressKeys(newEvidence, evidenceContract);
|
|
900
|
+
const recordedEvidenceKeys = new Set(g.progressEvidenceKeys ?? []);
|
|
901
|
+
const evidenceAdvanced = incomingEvidenceKeys.some(key => !recordedEvidenceKeys.has(key));
|
|
902
|
+
const hasAuditableProgressInputs = outcome.workspaceChanged !== undefined ||
|
|
903
|
+
outcome.evidenceRecords !== undefined ||
|
|
904
|
+
outcome.pendingPlanUpdate !== undefined;
|
|
905
|
+
// Text-only plan churn is still persisted as a snapshot for observability,
|
|
906
|
+
// but phase/next-action/description rewrites are not Goal progress. Runtime
|
|
907
|
+
// turns must present an auditable signal; legacy direct callers without the
|
|
908
|
+
// new metadata retain their explicit madeProgress value for compatibility.
|
|
909
|
+
const madeAuditableProgress = Boolean(outcome.workspaceChanged) || evidenceAdvanced || planAdvanced;
|
|
910
|
+
const accountingOutcome = {
|
|
911
|
+
...outcome,
|
|
912
|
+
madeProgress: hasAuditableProgressInputs ? madeAuditableProgress : outcome.madeProgress,
|
|
913
|
+
};
|
|
914
|
+
// Accumulate per-turn usage.
|
|
915
|
+
const acc = (0, accounting_1.accumulateTurn)({
|
|
916
|
+
tokensUsed: g.tokensUsed,
|
|
917
|
+
timeUsedMs: g.timeUsedMs,
|
|
918
|
+
continuationCount: g.continuationCount,
|
|
919
|
+
noProgressCount: g.noProgressCount,
|
|
920
|
+
}, accountingOutcome);
|
|
921
|
+
const tokensUsed = this.addGoalInteger(acc.tokensUsed, 0, 'tokensUsed');
|
|
922
|
+
const timeUsedMs = this.addGoalInteger(acc.timeUsedMs, 0, 'timeUsedMs');
|
|
923
|
+
const continuationCount = this.addGoalInteger(acc.continuationCount, 0, 'continuationCount');
|
|
924
|
+
const noProgressCount = this.addGoalInteger(acc.noProgressCount, 0, 'noProgressCount');
|
|
185
925
|
const updated = {
|
|
186
926
|
...g,
|
|
187
|
-
tokensUsed
|
|
188
|
-
timeUsedMs
|
|
189
|
-
continuationCount
|
|
190
|
-
noProgressCount
|
|
191
|
-
|
|
927
|
+
tokensUsed,
|
|
928
|
+
timeUsedMs,
|
|
929
|
+
continuationCount,
|
|
930
|
+
noProgressCount,
|
|
931
|
+
recentNoProgressTurns: accountingOutcome.madeProgress
|
|
932
|
+
? []
|
|
933
|
+
: [...(g.recentNoProgressTurns ?? []), noProgressTurnSummary(accountingOutcome)].slice(-3),
|
|
934
|
+
progressEvidenceKeys: [
|
|
935
|
+
...(g.progressEvidenceKeys ?? []),
|
|
936
|
+
...incomingEvidenceKeys.filter(key => !recordedEvidenceKeys.has(key)),
|
|
937
|
+
].slice(-1000),
|
|
938
|
+
revision: this.addGoalInteger(g.revision, 1, 'revision'),
|
|
192
939
|
updatedAt: Date.now(),
|
|
193
940
|
lastTurn: {
|
|
194
941
|
turnId: outcome.turnId,
|
|
@@ -198,38 +945,193 @@ class GoalCoordinator {
|
|
|
198
945
|
completionTokens: outcome.usage.completionTokens,
|
|
199
946
|
subagentTokens: outcome.usage.subagentTokens,
|
|
200
947
|
totalTokens: outcome.usage.totalTokens,
|
|
201
|
-
madeProgress:
|
|
948
|
+
madeProgress: accountingOutcome.madeProgress,
|
|
202
949
|
},
|
|
203
950
|
};
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
951
|
+
if (outcome.pendingPlanUpdate && pendingPlanMutatesContract) {
|
|
952
|
+
const contract = updated.contract ?? minimalContract(updated.objective, updated.updatedAt);
|
|
953
|
+
updated.contract = {
|
|
954
|
+
...contract,
|
|
955
|
+
successCriteria: [...contract.successCriteria, ...pendingDerivedCriteria],
|
|
956
|
+
planSnapshot: {
|
|
957
|
+
revision: pendingPlanRevision,
|
|
958
|
+
phase: outcome.pendingPlanUpdate.phase,
|
|
959
|
+
steps: outcome.pendingPlanUpdate.steps.map((step, index) => ({
|
|
960
|
+
id: `plan:${pendingPlanRevision}:${index + 1}`,
|
|
961
|
+
description: step.description,
|
|
962
|
+
done: step.done,
|
|
963
|
+
})),
|
|
964
|
+
nextAction: outcome.pendingPlanUpdate.nextAction,
|
|
965
|
+
updatedAt: Date.now(),
|
|
966
|
+
},
|
|
967
|
+
};
|
|
968
|
+
}
|
|
969
|
+
const boundedEvidence = appendBoundedEvidence(g.evidenceLedger ?? [], newEvidence, currentContract.objectiveRevision, g.evidenceLedgerTruncation);
|
|
970
|
+
const evidenceLedger = boundedEvidence.ledger;
|
|
971
|
+
updated.evidenceLedger = evidenceLedger;
|
|
972
|
+
updated.evidenceLedgerTruncation = boundedEvidence.truncation;
|
|
973
|
+
// Process a terminal request only when it belongs to this exact turn and
|
|
974
|
+
// goal revision. A stale tool call can never close a newer goal.
|
|
975
|
+
const terminalRequest = outcome.pendingTerminalRequest;
|
|
976
|
+
const currentBlockedSignal = Boolean(terminalRequest?.requestedStatus === 'blocked' &&
|
|
977
|
+
terminalRequest.goalId === g.goalId &&
|
|
978
|
+
terminalRequest.goalRevision === g.revision &&
|
|
979
|
+
terminalRequest.turnId === outcome.turnId &&
|
|
980
|
+
outcome.blocker &&
|
|
981
|
+
updated.blocker?.fingerprint === outcome.blocker.fingerprint);
|
|
982
|
+
// Consecutive means every Goal turn. Any intervening turn without the same
|
|
983
|
+
// current-turn blocker breaks the streak instead of leaving an old counter
|
|
984
|
+
// available for a later terminal request.
|
|
985
|
+
if (updated.blocker && !currentBlockedSignal)
|
|
986
|
+
updated.blocker = undefined;
|
|
987
|
+
if (terminalRequest &&
|
|
988
|
+
terminalRequest.goalId === g.goalId &&
|
|
989
|
+
terminalRequest.goalRevision === g.revision &&
|
|
990
|
+
terminalRequest.turnId === outcome.turnId) {
|
|
991
|
+
if (terminalRequest.requestedStatus === 'complete') {
|
|
992
|
+
if (updated.contract && terminalRequest.criterionEvidence) {
|
|
993
|
+
const ledgerById = new Map(evidenceLedger.map(record => [record.id, record]));
|
|
994
|
+
const criterionCounts = new Map();
|
|
995
|
+
const evidenceCounts = new Map();
|
|
996
|
+
for (const mapping of terminalRequest.criterionEvidence) {
|
|
997
|
+
criterionCounts.set(mapping.criterionId, (criterionCounts.get(mapping.criterionId) ?? 0) + 1);
|
|
998
|
+
for (const id of mapping.evidenceIds) {
|
|
999
|
+
evidenceCounts.set(id, (evidenceCounts.get(id) ?? 0) + 1);
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
1002
|
+
const mappings = new Map(terminalRequest.criterionEvidence.map(mapping => [
|
|
1003
|
+
mapping.criterionId,
|
|
1004
|
+
mapping.evidenceIds,
|
|
1005
|
+
]));
|
|
1006
|
+
updated.contract = {
|
|
1007
|
+
...updated.contract,
|
|
1008
|
+
successCriteria: updated.contract.successCriteria.map(criterion => {
|
|
1009
|
+
if (criterionCounts.get(criterion.id) !== 1)
|
|
1010
|
+
return criterion;
|
|
1011
|
+
const refs = (mappings.get(criterion.id) ?? []).filter(id => {
|
|
1012
|
+
const record = ledgerById.get(id);
|
|
1013
|
+
return Boolean(record &&
|
|
1014
|
+
evidenceCounts.get(id) === 1 &&
|
|
1015
|
+
record.goalId === g.goalId &&
|
|
1016
|
+
record.goalRevision <= g.revision &&
|
|
1017
|
+
record.objectiveRevision === (updated.contract?.objectiveRevision ?? 0) &&
|
|
1018
|
+
criterion.requiredEvidenceKinds.includes(record.kind));
|
|
1019
|
+
});
|
|
1020
|
+
return {
|
|
1021
|
+
...criterion,
|
|
1022
|
+
evidenceRefs: [...new Set([...criterion.evidenceRefs, ...refs])],
|
|
1023
|
+
};
|
|
1024
|
+
}),
|
|
1025
|
+
};
|
|
1026
|
+
}
|
|
207
1027
|
const audit = (0, completion_audit_1.auditCompletion)({
|
|
208
1028
|
objective: updated.objective,
|
|
209
|
-
|
|
210
|
-
|
|
1029
|
+
contract: updated.contract ?? minimalContract(updated.objective, updated.updatedAt),
|
|
1030
|
+
evidenceLedger,
|
|
1031
|
+
evidenceLedgerTruncation: updated.evidenceLedgerTruncation,
|
|
1032
|
+
goalId: g.goalId,
|
|
1033
|
+
goalRevision: g.revision,
|
|
1034
|
+
requestedAt: terminalRequest.requestedAt,
|
|
211
1035
|
verificationSummary: outcome.verificationSummary ?? outcome.finishReason,
|
|
1036
|
+
workspaceFingerprint: outcome.workspaceFingerprint,
|
|
212
1037
|
});
|
|
213
1038
|
updated.completionAudit = audit;
|
|
1039
|
+
if (updated.contract) {
|
|
1040
|
+
const statusById = new Map(audit.criterionResults?.map(result => [result.criterionId, result.status]) ?? []);
|
|
1041
|
+
updated.contract = {
|
|
1042
|
+
...updated.contract,
|
|
1043
|
+
successCriteria: updated.contract.successCriteria.map(criterion => ({
|
|
1044
|
+
...criterion,
|
|
1045
|
+
status: statusById.get(criterion.id) ?? 'pending',
|
|
1046
|
+
})),
|
|
1047
|
+
};
|
|
1048
|
+
}
|
|
214
1049
|
if (audit.passed) {
|
|
1050
|
+
const completedAt = Date.now();
|
|
1051
|
+
const criterionResults = audit.criterionResults ?? [];
|
|
1052
|
+
const finalLedgerById = new Map(evidenceLedger.map(record => [record.id, record]));
|
|
1053
|
+
updated.completionAudit = {
|
|
1054
|
+
...audit,
|
|
1055
|
+
finalSummary: {
|
|
1056
|
+
originalObjective: updated.contract?.originalObjective ?? updated.objective,
|
|
1057
|
+
currentObjective: updated.objective,
|
|
1058
|
+
objectiveRevision: updated.contract?.objectiveRevision ?? 0,
|
|
1059
|
+
completedAt,
|
|
1060
|
+
verificationSummary: audit.verificationSummary,
|
|
1061
|
+
criterionResults: criterionResults.map(result => ({
|
|
1062
|
+
criterionId: result.criterionId,
|
|
1063
|
+
status: result.status,
|
|
1064
|
+
evidenceRefs: result.evidenceRefs,
|
|
1065
|
+
evidence: result.evidenceRefs.flatMap(id => {
|
|
1066
|
+
const record = finalLedgerById.get(id);
|
|
1067
|
+
return record ? [finalEvidenceReceipt(record)] : [];
|
|
1068
|
+
}),
|
|
1069
|
+
})),
|
|
1070
|
+
evidenceRefs: audit.evidenceRefs,
|
|
1071
|
+
accounting: {
|
|
1072
|
+
tokensUsed: updated.tokensUsed,
|
|
1073
|
+
timeUsedMs: updated.timeUsedMs,
|
|
1074
|
+
continuationCount: updated.continuationCount,
|
|
1075
|
+
usageComplete: true,
|
|
1076
|
+
},
|
|
1077
|
+
remainingRequirements: [],
|
|
1078
|
+
stopReason: 'completed',
|
|
1079
|
+
},
|
|
1080
|
+
};
|
|
215
1081
|
updated.status = 'complete';
|
|
216
|
-
updated.completedAt =
|
|
1082
|
+
updated.completedAt = completedAt;
|
|
1083
|
+
delete updated.activeSince;
|
|
1084
|
+
delete updated.blocker;
|
|
1085
|
+
delete updated.stopReason;
|
|
1086
|
+
if (updated.contract) {
|
|
1087
|
+
const currentPlan = updated.contract.planSnapshot;
|
|
1088
|
+
updated.contract = {
|
|
1089
|
+
...updated.contract,
|
|
1090
|
+
planSnapshot: {
|
|
1091
|
+
revision: this.addGoalInteger(currentPlan?.revision ?? 0, 1, 'contract.planSnapshot.revision'),
|
|
1092
|
+
phase: 'complete',
|
|
1093
|
+
steps: currentPlan?.steps ?? [],
|
|
1094
|
+
nextAction: undefined,
|
|
1095
|
+
updatedAt: completedAt,
|
|
1096
|
+
},
|
|
1097
|
+
};
|
|
1098
|
+
}
|
|
1099
|
+
}
|
|
1100
|
+
else if (updated.contract) {
|
|
1101
|
+
const currentPlan = updated.contract.planSnapshot;
|
|
1102
|
+
updated.contract = {
|
|
1103
|
+
...updated.contract,
|
|
1104
|
+
planSnapshot: {
|
|
1105
|
+
revision: this.addGoalInteger(currentPlan?.revision ?? 0, 1, 'contract.planSnapshot.revision'),
|
|
1106
|
+
phase: 'verification',
|
|
1107
|
+
steps: currentPlan?.steps ?? [],
|
|
1108
|
+
nextAction: audit.remainingRequirements[0],
|
|
1109
|
+
updatedAt: Date.now(),
|
|
1110
|
+
},
|
|
1111
|
+
};
|
|
217
1112
|
}
|
|
218
1113
|
}
|
|
219
|
-
else if (
|
|
1114
|
+
else if (terminalRequest.requestedStatus === 'blocked') {
|
|
220
1115
|
const blocker = outcome.blocker;
|
|
221
1116
|
if (blocker) {
|
|
222
|
-
const fingerprint =
|
|
1117
|
+
const fingerprint = blocker.fingerprint;
|
|
223
1118
|
const same = (0, completion_audit_1.blockersMatch)(updated.blocker, fingerprint);
|
|
224
1119
|
const newBlocker = {
|
|
1120
|
+
category: blocker.category,
|
|
225
1121
|
fingerprint,
|
|
226
1122
|
firstSeenAt: same && updated.blocker ? updated.blocker.firstSeenAt : Date.now(),
|
|
227
1123
|
lastSeenAt: Date.now(),
|
|
228
|
-
consecutiveTurns: same
|
|
1124
|
+
consecutiveTurns: same
|
|
1125
|
+
? this.addGoalInteger(updated.blocker?.consecutiveTurns ?? 0, 1, 'blocker.consecutiveTurns')
|
|
1126
|
+
: 1,
|
|
229
1127
|
summary: blocker.summary,
|
|
1128
|
+
retryable: false,
|
|
230
1129
|
};
|
|
231
1130
|
updated.blocker = newBlocker;
|
|
232
|
-
const blockAudit = (0, completion_audit_1.auditBlocked)({
|
|
1131
|
+
const blockAudit = (0, completion_audit_1.auditBlocked)({
|
|
1132
|
+
blocker: newBlocker,
|
|
1133
|
+
noProgressCount: updated.noProgressCount,
|
|
1134
|
+
});
|
|
233
1135
|
if (blockAudit.allowed) {
|
|
234
1136
|
updated.status = 'blocked';
|
|
235
1137
|
updated.stopReason = { kind: 'blocked', message: blockAudit.reason, at: Date.now() };
|
|
@@ -237,20 +1139,115 @@ class GoalCoordinator {
|
|
|
237
1139
|
}
|
|
238
1140
|
}
|
|
239
1141
|
}
|
|
1142
|
+
if (updated.lastTurn &&
|
|
1143
|
+
!updated.lastTurn.madeProgress &&
|
|
1144
|
+
criterionStateAdvanced(currentContract, updated.contract, evidenceLedger)) {
|
|
1145
|
+
updated.noProgressCount = 0;
|
|
1146
|
+
updated.recentNoProgressTurns = [];
|
|
1147
|
+
updated.lastTurn = { ...updated.lastTurn, madeProgress: true };
|
|
1148
|
+
}
|
|
1149
|
+
const providerStop = (0, accounting_1.classifyStopReason)(outcome.providerError);
|
|
1150
|
+
if (providerStop && updated.status === 'active') {
|
|
1151
|
+
updated.status = providerStop.kind === 'usage_limit' ? 'usage_limited' : 'paused';
|
|
1152
|
+
updated.stopReason = {
|
|
1153
|
+
kind: providerStop.kind,
|
|
1154
|
+
message: providerStop.message,
|
|
1155
|
+
at: Date.now(),
|
|
1156
|
+
};
|
|
1157
|
+
this.state.continuationDeferred = true;
|
|
1158
|
+
}
|
|
240
1159
|
// Auto-pause on no-progress threshold.
|
|
241
|
-
if (updated.noProgressCount >= types_1.GOAL_INVARIANTS.maxConsecutiveNoProgressTurns &&
|
|
1160
|
+
if (updated.noProgressCount >= types_1.GOAL_INVARIANTS.maxConsecutiveNoProgressTurns &&
|
|
1161
|
+
updated.status === 'active') {
|
|
242
1162
|
updated.status = 'paused';
|
|
243
|
-
|
|
1163
|
+
const recent = updated.recentNoProgressTurns ?? [];
|
|
1164
|
+
updated.stopReason = {
|
|
1165
|
+
kind: 'user',
|
|
1166
|
+
message: `Auto-paused: no progress for 3 consecutive turns. Recent turns: ${formatNoProgressTurns(recent)}.`,
|
|
1167
|
+
at: Date.now(),
|
|
1168
|
+
};
|
|
244
1169
|
this.state.continuationDeferred = true;
|
|
245
1170
|
}
|
|
246
1171
|
// Budget limit.
|
|
247
1172
|
if ((0, accounting_1.isBudgetExceeded)(updated.tokensUsed, updated.tokenBudget) && updated.status === 'active') {
|
|
248
1173
|
updated.status = 'budget_limited';
|
|
249
|
-
updated.stopReason = {
|
|
1174
|
+
updated.stopReason = {
|
|
1175
|
+
kind: 'budget_limit',
|
|
1176
|
+
message: `Token budget reached: ${updated.tokensUsed}/${updated.tokenBudget}`,
|
|
1177
|
+
at: Date.now(),
|
|
1178
|
+
};
|
|
250
1179
|
}
|
|
251
1180
|
this.state.goal = updated;
|
|
252
1181
|
this.persist();
|
|
253
1182
|
}
|
|
1183
|
+
/**
|
|
1184
|
+
* Account an interrupted turn whose request revision was superseded by live steering.
|
|
1185
|
+
*
|
|
1186
|
+
* This deliberately ignores every semantic payload on the stale outcome: evidence,
|
|
1187
|
+
* plan updates, blockers, and terminal requests belong to the old contract revision.
|
|
1188
|
+
* Only already-incurred token/time/turn accounting is retained, without changing the
|
|
1189
|
+
* revised Goal's no-progress streak.
|
|
1190
|
+
*/
|
|
1191
|
+
accountStaleTurn(outcome) {
|
|
1192
|
+
const g = this.state.goal;
|
|
1193
|
+
if (!g ||
|
|
1194
|
+
outcome.sessionId !== this.sessionId ||
|
|
1195
|
+
outcome.goalId !== g.goalId ||
|
|
1196
|
+
outcome.goalRevision === undefined ||
|
|
1197
|
+
outcome.goalRevision >= g.revision ||
|
|
1198
|
+
(outcome.goalGeneration !== undefined && outcome.goalGeneration !== this.state.generation)) {
|
|
1199
|
+
return false;
|
|
1200
|
+
}
|
|
1201
|
+
const key = `${outcome.goalId}:${outcome.goalGeneration ?? this.state.generation}:${outcome.turnId}`;
|
|
1202
|
+
if (this.accountedStaleTurnKeys.has(key))
|
|
1203
|
+
return false;
|
|
1204
|
+
const endedAt = Math.max(outcome.startedAt, outcome.endedAt);
|
|
1205
|
+
const elapsedMs = endedAt - outcome.startedAt;
|
|
1206
|
+
const tokensUsed = this.addGoalInteger(g.tokensUsed, Math.max(0, outcome.usage.totalTokens), 'tokensUsed');
|
|
1207
|
+
const timeUsedMs = this.addGoalInteger(g.timeUsedMs, elapsedMs, 'timeUsedMs');
|
|
1208
|
+
const continuationCount = this.addGoalInteger(g.continuationCount, 1, 'continuationCount');
|
|
1209
|
+
const revision = this.addGoalInteger(g.revision, 1, 'revision');
|
|
1210
|
+
this.accountedStaleTurnKeys.add(key);
|
|
1211
|
+
const updated = {
|
|
1212
|
+
...g,
|
|
1213
|
+
tokensUsed,
|
|
1214
|
+
timeUsedMs,
|
|
1215
|
+
continuationCount,
|
|
1216
|
+
revision,
|
|
1217
|
+
updatedAt: Date.now(),
|
|
1218
|
+
lastTurn: {
|
|
1219
|
+
turnId: outcome.turnId,
|
|
1220
|
+
finishReason: outcome.finishReason,
|
|
1221
|
+
endedAt,
|
|
1222
|
+
promptTokens: Math.max(0, outcome.usage.promptTokens),
|
|
1223
|
+
completionTokens: Math.max(0, outcome.usage.completionTokens),
|
|
1224
|
+
subagentTokens: Math.max(0, outcome.usage.subagentTokens),
|
|
1225
|
+
totalTokens: Math.max(0, outcome.usage.totalTokens),
|
|
1226
|
+
madeProgress: false,
|
|
1227
|
+
},
|
|
1228
|
+
};
|
|
1229
|
+
if ((0, accounting_1.isBudgetExceeded)(updated.tokensUsed, updated.tokenBudget) && updated.status === 'active') {
|
|
1230
|
+
updated.status = 'budget_limited';
|
|
1231
|
+
updated.stopReason = {
|
|
1232
|
+
kind: 'budget_limit',
|
|
1233
|
+
message: `Token budget reached: ${updated.tokensUsed}/${updated.tokenBudget}`,
|
|
1234
|
+
at: Date.now(),
|
|
1235
|
+
};
|
|
1236
|
+
this.state.continuationDeferred = true;
|
|
1237
|
+
}
|
|
1238
|
+
this.state.goal = updated;
|
|
1239
|
+
try {
|
|
1240
|
+
this.persist();
|
|
1241
|
+
}
|
|
1242
|
+
catch (error) {
|
|
1243
|
+
// The key protects an in-flight attempt, but a failed pre-commit save did
|
|
1244
|
+
// not durably account the turn. Release it only after persist() restores
|
|
1245
|
+
// disk authority so the same outcome can be retried exactly once.
|
|
1246
|
+
this.accountedStaleTurnKeys.delete(key);
|
|
1247
|
+
throw error;
|
|
1248
|
+
}
|
|
1249
|
+
return true;
|
|
1250
|
+
}
|
|
254
1251
|
deferContinuation() {
|
|
255
1252
|
if (!this.state.goal || this.state.goal.status !== 'active')
|
|
256
1253
|
return;
|
|
@@ -259,19 +1256,88 @@ class GoalCoordinator {
|
|
|
259
1256
|
this.state.goal = {
|
|
260
1257
|
...g,
|
|
261
1258
|
status: 'paused',
|
|
262
|
-
revision: g.revision
|
|
1259
|
+
revision: this.addGoalInteger(g.revision, 1, 'revision'),
|
|
263
1260
|
updatedAt: Date.now(),
|
|
264
1261
|
stopReason: { kind: 'user', message: 'Paused by interrupt.', at: Date.now() },
|
|
265
1262
|
};
|
|
266
1263
|
this.persist();
|
|
267
1264
|
}
|
|
1265
|
+
limitBudget(reason) {
|
|
1266
|
+
const goal = this.state.goal;
|
|
1267
|
+
if (!goal || goal.status !== 'active')
|
|
1268
|
+
return;
|
|
1269
|
+
this.state.goal = {
|
|
1270
|
+
...goal,
|
|
1271
|
+
status: 'budget_limited',
|
|
1272
|
+
revision: this.addGoalInteger(goal.revision, 1, 'revision'),
|
|
1273
|
+
updatedAt: Date.now(),
|
|
1274
|
+
stopReason: { kind: 'budget_limit', message: reason, at: Date.now() },
|
|
1275
|
+
};
|
|
1276
|
+
this.state.continuationDeferred = true;
|
|
1277
|
+
this.persist();
|
|
1278
|
+
}
|
|
1279
|
+
/**
|
|
1280
|
+
* Apply a runtime-only safety overlay after a mutation failed to persist.
|
|
1281
|
+
* `persist()` has already restored the disk-authoritative Goal. Keep its
|
|
1282
|
+
* revision intact so `/target resume` can save normally once storage recovers,
|
|
1283
|
+
* while invalidating every in-flight request and queued continuation now.
|
|
1284
|
+
*/
|
|
1285
|
+
failClosedAfterPersistenceError(reason) {
|
|
1286
|
+
const goal = this.state.goal;
|
|
1287
|
+
this.state.generation += 1;
|
|
1288
|
+
this.state.continuationDeferred = true;
|
|
1289
|
+
if (!goal || goal.status === 'complete')
|
|
1290
|
+
return;
|
|
1291
|
+
this.state.goal = {
|
|
1292
|
+
...goal,
|
|
1293
|
+
status: 'paused',
|
|
1294
|
+
updatedAt: Date.now(),
|
|
1295
|
+
stopReason: {
|
|
1296
|
+
kind: 'runtime_error',
|
|
1297
|
+
message: (0, redaction_1.redactTraceText)(reason).slice(0, 600),
|
|
1298
|
+
at: Date.now(),
|
|
1299
|
+
},
|
|
1300
|
+
};
|
|
1301
|
+
}
|
|
268
1302
|
// ==========================================================================
|
|
269
1303
|
// Persistence
|
|
270
1304
|
// ==========================================================================
|
|
1305
|
+
/** Restore disk authority without reviving a deletion-fenced Goal. */
|
|
1306
|
+
restoreDiskAuthorityAfterMutationFailure() {
|
|
1307
|
+
const persisted = (0, goal_storage_1.loadGoal)(this.projectPath, this.sessionId);
|
|
1308
|
+
if (persisted.ok) {
|
|
1309
|
+
this.state.goal = ensureContract(persisted.value);
|
|
1310
|
+
this.persistedGoalSnapshot = structuredClone(this.state.goal);
|
|
1311
|
+
this.state.continuationDeferred = true;
|
|
1312
|
+
return;
|
|
1313
|
+
}
|
|
1314
|
+
if (persisted.error === 'not_found') {
|
|
1315
|
+
this.state.goal = null;
|
|
1316
|
+
this.persistedGoalSnapshot = null;
|
|
1317
|
+
this.state.generation += 1;
|
|
1318
|
+
this.state.continuationDeferred = false;
|
|
1319
|
+
return;
|
|
1320
|
+
}
|
|
1321
|
+
if (this.persistedGoalSnapshot) {
|
|
1322
|
+
this.state.goal = structuredClone(this.persistedGoalSnapshot);
|
|
1323
|
+
}
|
|
1324
|
+
else {
|
|
1325
|
+
this.state.goal = null;
|
|
1326
|
+
}
|
|
1327
|
+
this.state.continuationDeferred = true;
|
|
1328
|
+
}
|
|
271
1329
|
persist() {
|
|
272
1330
|
if (!this.state.goal)
|
|
273
1331
|
return;
|
|
274
|
-
(0, goal_storage_1.saveGoal)(this.projectPath, this.sessionId, this.state.goal);
|
|
1332
|
+
const result = (0, goal_storage_1.saveGoal)(this.projectPath, this.sessionId, this.state.goal, Math.max(0, this.state.goal.revision - 1));
|
|
1333
|
+
if (result.ok) {
|
|
1334
|
+
this.persistedGoalSnapshot = structuredClone(this.state.goal);
|
|
1335
|
+
return;
|
|
1336
|
+
}
|
|
1337
|
+
if (!result.ok) {
|
|
1338
|
+
this.restoreDiskAuthorityAfterMutationFailure();
|
|
1339
|
+
throw new Error(`Goal persistence failed (${result.error}): ${result.message}`);
|
|
1340
|
+
}
|
|
275
1341
|
}
|
|
276
1342
|
}
|
|
277
1343
|
exports.GoalCoordinator = GoalCoordinator;
|