@phyzess/pappra 0.0.0-stage → 0.1.0-beta.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/LICENSE +21 -0
- package/README.md +80 -2
- package/THIRD_PARTY_NOTICES.md +16 -0
- package/assets/integrations/skills/pappra/SKILL.md +68 -0
- package/assets/integrations/skills/pappra/references/hosts.md +30 -0
- package/assets/integrations/skills/pappra/references/lifecycle.md +37 -0
- package/assets/integrations/skills/pappra/references/operations.md +14 -0
- package/assets/manifest.json +4 -0
- package/dist/app/collaboration.d.ts +170 -0
- package/dist/app/collaboration.js +1650 -0
- package/dist/app/doctor.d.ts +9 -0
- package/dist/app/doctor.js +1030 -0
- package/dist/app/init.d.ts +143 -0
- package/dist/app/init.js +1111 -0
- package/dist/cli/commands/code.d.ts +16 -0
- package/dist/cli/commands/code.js +127 -0
- package/dist/cli/commands/collaboration.d.ts +13 -0
- package/dist/cli/commands/collaboration.js +186 -0
- package/dist/cli/commands/continuity.d.ts +29 -0
- package/dist/cli/commands/continuity.js +133 -0
- package/dist/cli/commands/docs.d.ts +11 -0
- package/dist/cli/commands/docs.js +133 -0
- package/dist/cli/commands/doctor.d.ts +14 -0
- package/dist/cli/commands/doctor.js +57 -0
- package/dist/cli/commands/host.d.ts +13 -0
- package/dist/cli/commands/host.js +125 -0
- package/dist/cli/commands/init.d.ts +51 -0
- package/dist/cli/commands/init.js +668 -0
- package/dist/cli/main.d.ts +2 -0
- package/dist/cli/main.js +10 -0
- package/dist/cli/output.d.ts +4 -0
- package/dist/cli/output.js +1 -0
- package/dist/cli/run.d.ts +2 -0
- package/dist/cli/run.js +597 -0
- package/dist/code/budget.d.ts +14 -0
- package/dist/code/budget.js +39 -0
- package/dist/code/exec.d.ts +23 -0
- package/dist/code/exec.js +133 -0
- package/dist/code/identity.d.ts +13 -0
- package/dist/code/identity.js +214 -0
- package/dist/code/index.d.ts +6 -0
- package/dist/code/index.js +3 -0
- package/dist/code/providers/codegraph.d.ts +59 -0
- package/dist/code/providers/codegraph.js +567 -0
- package/dist/code/registration.d.ts +9 -0
- package/dist/code/registration.js +293 -0
- package/dist/code/service.d.ts +53 -0
- package/dist/code/service.js +633 -0
- package/dist/code/text.d.ts +3 -0
- package/dist/code/text.js +202 -0
- package/dist/collab/admission.d.ts +450 -0
- package/dist/collab/admission.js +797 -0
- package/dist/collab/canonical.d.ts +26 -0
- package/dist/collab/canonical.js +136 -0
- package/dist/collab/contracts.d.ts +73 -0
- package/dist/collab/contracts.js +76 -0
- package/dist/collab/envelope.d.ts +52 -0
- package/dist/collab/envelope.js +241 -0
- package/dist/collab/fingerprint.d.ts +7 -0
- package/dist/collab/fingerprint.js +58 -0
- package/dist/collab/handoff.d.ts +350 -0
- package/dist/collab/handoff.js +513 -0
- package/dist/collab/index.d.ts +9 -0
- package/dist/collab/index.js +8 -0
- package/dist/collab/lifecycle.d.ts +449 -0
- package/dist/collab/lifecycle.js +534 -0
- package/dist/collab/messages.d.ts +148 -0
- package/dist/collab/messages.js +583 -0
- package/dist/collab/notify.d.ts +47 -0
- package/dist/collab/notify.js +140 -0
- package/dist/collab/preflight.d.ts +31 -0
- package/dist/collab/preflight.js +271 -0
- package/dist/collab/resources.d.ts +119 -0
- package/dist/collab/resources.js +451 -0
- package/dist/collab/store.d.ts +143 -0
- package/dist/collab/store.js +606 -0
- package/dist/collab/types.d.ts +354 -0
- package/dist/collab/types.js +1 -0
- package/dist/collab/validation.d.ts +10 -0
- package/dist/collab/validation.js +303 -0
- package/dist/continuity/context.d.ts +24 -0
- package/dist/continuity/context.js +179 -0
- package/dist/continuity/index.d.ts +3 -0
- package/dist/continuity/index.js +1 -0
- package/dist/continuity/paths.d.ts +37 -0
- package/dist/continuity/paths.js +258 -0
- package/dist/continuity/service.d.ts +59 -0
- package/dist/continuity/service.js +1258 -0
- package/dist/hosts/claude/native.d.ts +79 -0
- package/dist/hosts/claude/native.js +853 -0
- package/dist/hosts/codex/native.d.ts +345 -0
- package/dist/hosts/codex/native.js +1528 -0
- package/dist/hosts/codex/protocol.d.ts +51 -0
- package/dist/hosts/codex/protocol.js +42 -0
- package/dist/hosts/detect.d.ts +28 -0
- package/dist/hosts/detect.js +73 -0
- package/dist/hosts/index.d.ts +10 -0
- package/dist/hosts/index.js +10 -0
- package/dist/hosts/integration/bundle.d.ts +16 -0
- package/dist/hosts/integration/bundle.js +81 -0
- package/dist/hosts/integration/hook-entry.d.ts +1 -0
- package/dist/hosts/integration/hook-entry.js +68 -0
- package/dist/hosts/integration/hook.d.ts +32 -0
- package/dist/hosts/integration/hook.js +781 -0
- package/dist/hosts/integration/pi-extension-runtime.d.ts +18 -0
- package/dist/hosts/integration/pi-extension-runtime.js +89 -0
- package/dist/hosts/integration/pi-template.d.ts +3 -0
- package/dist/hosts/integration/pi-template.js +5 -0
- package/dist/hosts/integration/service.d.ts +41 -0
- package/dist/hosts/integration/service.js +1381 -0
- package/dist/hosts/integration/support.d.ts +36 -0
- package/dist/hosts/integration/support.js +41 -0
- package/dist/hosts/integration/templates.d.ts +22 -0
- package/dist/hosts/integration/templates.js +70 -0
- package/dist/hosts/pi/protocol.d.ts +22 -0
- package/dist/hosts/pi/protocol.js +28 -0
- package/dist/hosts/pi/query-result.d.ts +22 -0
- package/dist/hosts/pi/query-result.js +710 -0
- package/dist/hosts/pi/rpc.d.ts +937 -0
- package/dist/hosts/pi/rpc.js +1621 -0
- package/dist/hosts/pilot.d.ts +485 -0
- package/dist/hosts/pilot.js +593 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +4 -0
- package/dist/infra/fs.d.ts +27 -0
- package/dist/infra/fs.js +130 -0
- package/dist/infra/git.d.ts +60 -0
- package/dist/infra/git.js +358 -0
- package/dist/infra/owned-process-group.d.ts +173 -0
- package/dist/infra/owned-process-group.js +571 -0
- package/dist/install/apply.d.ts +65 -0
- package/dist/install/apply.js +663 -0
- package/dist/install/index.d.ts +5 -0
- package/dist/install/index.js +5 -0
- package/dist/install/ownership.d.ts +85 -0
- package/dist/install/ownership.js +430 -0
- package/dist/install/plan.d.ts +39 -0
- package/dist/install/plan.js +97 -0
- package/dist/install/rollback.d.ts +18 -0
- package/dist/install/rollback.js +544 -0
- package/dist/install/runtime.d.ts +76 -0
- package/dist/install/runtime.js +1145 -0
- package/dist/knowledge/catalog.d.ts +47 -0
- package/dist/knowledge/catalog.js +308 -0
- package/dist/knowledge/context.d.ts +28 -0
- package/dist/knowledge/context.js +61 -0
- package/dist/knowledge/index.d.ts +3 -0
- package/dist/knowledge/index.js +1 -0
- package/dist/knowledge/journal.d.ts +16 -0
- package/dist/knowledge/journal.js +109 -0
- package/dist/knowledge/lifecycle.d.ts +8 -0
- package/dist/knowledge/lifecycle.js +1001 -0
- package/dist/knowledge/paths.d.ts +37 -0
- package/dist/knowledge/paths.js +236 -0
- package/dist/knowledge/refs.d.ts +36 -0
- package/dist/knowledge/refs.js +267 -0
- package/dist/knowledge/service.d.ts +21 -0
- package/dist/knowledge/service.js +393 -0
- package/dist/knowledge/types.d.ts +171 -0
- package/dist/knowledge/types.js +1 -0
- package/dist/knowledge/write.d.ts +8 -0
- package/dist/knowledge/write.js +1223 -0
- package/dist/package-info.d.ts +6 -0
- package/dist/package-info.js +19 -0
- package/dist/schemas/code.d.ts +377 -0
- package/dist/schemas/code.js +145 -0
- package/dist/schemas/collaboration.d.ts +115 -0
- package/dist/schemas/collaboration.js +106 -0
- package/dist/schemas/continuity.d.ts +315 -0
- package/dist/schemas/continuity.js +199 -0
- package/dist/schemas/diagnostics.d.ts +28 -0
- package/dist/schemas/diagnostics.js +27 -0
- package/dist/schemas/doctor.d.ts +157 -0
- package/dist/schemas/doctor.js +75 -0
- package/dist/schemas/document.d.ts +644 -0
- package/dist/schemas/document.js +296 -0
- package/dist/schemas/errors.d.ts +177 -0
- package/dist/schemas/errors.js +176 -0
- package/dist/schemas/evidence.d.ts +24 -0
- package/dist/schemas/evidence.js +16 -0
- package/dist/schemas/host.d.ts +436 -0
- package/dist/schemas/host.js +201 -0
- package/dist/schemas/index.d.ts +15 -0
- package/dist/schemas/index.js +15 -0
- package/dist/schemas/init.d.ts +121 -0
- package/dist/schemas/init.js +94 -0
- package/dist/schemas/install.d.ts +213 -0
- package/dist/schemas/install.js +139 -0
- package/dist/schemas/machine.d.ts +43 -0
- package/dist/schemas/machine.js +96 -0
- package/dist/schemas/outcome.d.ts +23 -0
- package/dist/schemas/outcome.js +17 -0
- package/dist/schemas/paths.d.ts +19 -0
- package/dist/schemas/paths.js +63 -0
- package/dist/schemas/workspace.d.ts +120 -0
- package/dist/schemas/workspace.js +201 -0
- package/dist/workspace/config-file.d.ts +36 -0
- package/dist/workspace/config-file.js +237 -0
- package/dist/workspace/config.d.ts +28 -0
- package/dist/workspace/config.js +79 -0
- package/dist/workspace/discover.d.ts +34 -0
- package/dist/workspace/discover.js +195 -0
- package/dist/workspace/documents.d.ts +21 -0
- package/dist/workspace/documents.js +395 -0
- package/dist/workspace/ignore.d.ts +10 -0
- package/dist/workspace/ignore.js +48 -0
- package/dist/workspace/index.d.ts +7 -0
- package/dist/workspace/index.js +3 -0
- package/dist/workspace/repositories.d.ts +24 -0
- package/dist/workspace/repositories.js +411 -0
- package/dist/workspace/resolve.d.ts +24 -0
- package/dist/workspace/resolve.js +268 -0
- package/dist/workspace/types.d.ts +71 -0
- package/dist/workspace/types.js +1 -0
- package/dist/workspace/validation.d.ts +18 -0
- package/dist/workspace/validation.js +197 -0
- package/package.json +105 -4
- package/skills/pappra/SKILL.md +68 -0
- package/skills/pappra/references/hosts.md +30 -0
- package/skills/pappra/references/lifecycle.md +37 -0
- package/skills/pappra/references/operations.md +14 -0
|
@@ -0,0 +1,534 @@
|
|
|
1
|
+
import { text, object } from './validation.js';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { createNonce, digestText, nowIso, resultsDir } from './store.js';
|
|
5
|
+
import { canonicalJson } from './envelope.js';
|
|
6
|
+
// Attempt lifecycle, native binding, result admission and the uncertainty ledger.
|
|
7
|
+
//
|
|
8
|
+
// The record never claims more than it knows: an attempt whose native execution could
|
|
9
|
+
// not be confirmed stays `unknown`, keeps its resources, and is reconciled later using
|
|
10
|
+
// evidence. Late or superseded results are preserved as evidence and never overwrite a
|
|
11
|
+
// newer conclusion.
|
|
12
|
+
export const ATTEMPT_STATES = Object.freeze([
|
|
13
|
+
'assigned',
|
|
14
|
+
'accepted',
|
|
15
|
+
'running',
|
|
16
|
+
'completed',
|
|
17
|
+
'needs_input',
|
|
18
|
+
'failed',
|
|
19
|
+
'cancelled',
|
|
20
|
+
'unknown',
|
|
21
|
+
]);
|
|
22
|
+
export const ACTIVE_STATES = Object.freeze([
|
|
23
|
+
'assigned',
|
|
24
|
+
'accepted',
|
|
25
|
+
'running',
|
|
26
|
+
'unknown',
|
|
27
|
+
]);
|
|
28
|
+
export const TERMINAL_STATES = Object.freeze([
|
|
29
|
+
'completed',
|
|
30
|
+
'needs_input',
|
|
31
|
+
'failed',
|
|
32
|
+
'cancelled',
|
|
33
|
+
]);
|
|
34
|
+
/** Outcomes that genuinely end an execution. `needs_input` is a recoverable block, so it is
|
|
35
|
+
* terminal for transitions but NOT closed for the purpose of starting a new attempt. */
|
|
36
|
+
export const CLOSED_STATES = Object.freeze([
|
|
37
|
+
'completed',
|
|
38
|
+
'failed',
|
|
39
|
+
'cancelled',
|
|
40
|
+
]);
|
|
41
|
+
const TRANSITIONS = {
|
|
42
|
+
assigned: ['accepted', 'running', 'needs_input', 'failed', 'cancelled'],
|
|
43
|
+
accepted: ['running', 'needs_input', 'failed', 'cancelled'],
|
|
44
|
+
running: ['completed', 'needs_input', 'failed', 'cancelled', 'unknown'],
|
|
45
|
+
unknown: ['running', 'completed', 'needs_input', 'failed', 'cancelled'],
|
|
46
|
+
needs_input: ['running', 'cancelled'],
|
|
47
|
+
completed: [],
|
|
48
|
+
failed: [],
|
|
49
|
+
cancelled: [],
|
|
50
|
+
};
|
|
51
|
+
export const UNCERTAINTY_KINDS = Object.freeze([
|
|
52
|
+
'registered_not_started',
|
|
53
|
+
'started_unregistered',
|
|
54
|
+
'executed_unwritten',
|
|
55
|
+
'notification_lost',
|
|
56
|
+
'cancel_unconfirmed',
|
|
57
|
+
'session_identity_drift',
|
|
58
|
+
'session_unobserved',
|
|
59
|
+
'session_unverified',
|
|
60
|
+
'late_result',
|
|
61
|
+
'stale_epoch',
|
|
62
|
+
]);
|
|
63
|
+
export function attemptKey(subtaskId, attempt) {
|
|
64
|
+
return `${subtaskId}#${attempt}`;
|
|
65
|
+
}
|
|
66
|
+
export function findAttempt(record, subtaskId, attempt) {
|
|
67
|
+
return ((record.attempts ?? []).find((item) => item.subtask_id === subtaskId && item.attempt === attempt) ?? null);
|
|
68
|
+
}
|
|
69
|
+
export function attemptsFor(record, subtaskId) {
|
|
70
|
+
return (record.attempts ?? [])
|
|
71
|
+
.filter((item) => item.subtask_id === subtaskId)
|
|
72
|
+
.sort((a, b) => a.attempt - b.attempt);
|
|
73
|
+
}
|
|
74
|
+
export function latestAttemptNumber(record, subtaskId) {
|
|
75
|
+
const all = attemptsFor(record, subtaskId);
|
|
76
|
+
return all.at(-1)?.attempt ?? 0;
|
|
77
|
+
}
|
|
78
|
+
export function checkOwnerEpoch(record, epoch) {
|
|
79
|
+
if (!Number.isSafeInteger(epoch) || epoch <= 0)
|
|
80
|
+
return { ok: false, reason: 'invalid_epoch' };
|
|
81
|
+
const current = record.coordinator?.owner_epoch ?? 0;
|
|
82
|
+
if (epoch !== current)
|
|
83
|
+
return {
|
|
84
|
+
ok: false,
|
|
85
|
+
reason: 'stale_epoch',
|
|
86
|
+
expected: current,
|
|
87
|
+
actual: epoch,
|
|
88
|
+
};
|
|
89
|
+
return { ok: true, owner_epoch: current };
|
|
90
|
+
}
|
|
91
|
+
/** Every state change is appended as a transition record; history is never rewritten. */
|
|
92
|
+
export function transitionAttempt(record, { subtaskId, attempt, to, actor, reason, evidence = null, clock = Date.now(), force = false, }) {
|
|
93
|
+
const target = findAttempt(record, subtaskId, attempt);
|
|
94
|
+
if (!target)
|
|
95
|
+
return {
|
|
96
|
+
ok: false,
|
|
97
|
+
reason: 'unknown_attempt',
|
|
98
|
+
subtask_id: subtaskId,
|
|
99
|
+
attempt,
|
|
100
|
+
};
|
|
101
|
+
if (!ATTEMPT_STATES.includes(to))
|
|
102
|
+
return { ok: false, reason: 'unknown_state', state: to };
|
|
103
|
+
if (target.state === to)
|
|
104
|
+
return { ok: false, reason: 'already_in_state', state: to };
|
|
105
|
+
const allowed = TRANSITIONS[target.state] ?? [];
|
|
106
|
+
if (!allowed.includes(to))
|
|
107
|
+
return {
|
|
108
|
+
ok: false,
|
|
109
|
+
reason: 'invalid_transition',
|
|
110
|
+
from: target.state,
|
|
111
|
+
to,
|
|
112
|
+
};
|
|
113
|
+
if (target.state === 'needs_input' &&
|
|
114
|
+
to === 'running' &&
|
|
115
|
+
target.awaiting === 'approval' &&
|
|
116
|
+
!(object(evidence) && evidence.authorization)) {
|
|
117
|
+
return {
|
|
118
|
+
ok: false,
|
|
119
|
+
reason: 'approval_required',
|
|
120
|
+
detail: 'an approval block is answered by the user, not by another agent',
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
const at = nowIso(clock);
|
|
124
|
+
target.state = to;
|
|
125
|
+
target.updated_at = at;
|
|
126
|
+
target.transitions = target.transitions ?? [];
|
|
127
|
+
target.transitions.push({
|
|
128
|
+
from: target.transitions.at(-1)?.to ?? null,
|
|
129
|
+
to,
|
|
130
|
+
at,
|
|
131
|
+
actor: actor ?? 'unattributed',
|
|
132
|
+
reason: reason ?? null,
|
|
133
|
+
evidence,
|
|
134
|
+
forced: force || undefined,
|
|
135
|
+
});
|
|
136
|
+
if (TERMINAL_STATES.includes(to))
|
|
137
|
+
target.closed_at = at;
|
|
138
|
+
return { ok: true, state: to, at };
|
|
139
|
+
}
|
|
140
|
+
export function upsertAssignment(record, { message, assignmentRef = null, assignmentDigest = null, inputRevision = null, clock = Date.now(), }) {
|
|
141
|
+
const { subtask_id: subtaskId, attempt } = message;
|
|
142
|
+
if (!subtaskId || !attempt)
|
|
143
|
+
return { ok: false, reason: 'assignment_requires_attempt' };
|
|
144
|
+
const existing = findAttempt(record, subtaskId, attempt);
|
|
145
|
+
if (existing) {
|
|
146
|
+
if (existing.assignment_message_id === message.message_id) {
|
|
147
|
+
return {
|
|
148
|
+
ok: false,
|
|
149
|
+
reason: 'duplicate_assignment',
|
|
150
|
+
subtask_id: subtaskId,
|
|
151
|
+
attempt,
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
if (ACTIVE_STATES.includes(existing.state) ||
|
|
155
|
+
TERMINAL_STATES.includes(existing.state)) {
|
|
156
|
+
return {
|
|
157
|
+
ok: false,
|
|
158
|
+
reason: 'attempt_in_flight',
|
|
159
|
+
state: existing.state,
|
|
160
|
+
subtask_id: subtaskId,
|
|
161
|
+
attempt,
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
const latest = latestAttemptNumber(record, subtaskId);
|
|
166
|
+
if (attempt > latest + 1) {
|
|
167
|
+
return {
|
|
168
|
+
ok: false,
|
|
169
|
+
reason: 'attempt_skips_sequence',
|
|
170
|
+
latest,
|
|
171
|
+
requested: attempt,
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
if (latest > 0 && attempt > latest) {
|
|
175
|
+
const approval = message.body?.approval;
|
|
176
|
+
if (!approval) {
|
|
177
|
+
return {
|
|
178
|
+
ok: false,
|
|
179
|
+
reason: 'attempt_requires_approval',
|
|
180
|
+
detail: 'a new attempt for an existing subtask needs an explicit approval record',
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
// An approval authorises a retry; it does not prove the previous execution stopped. The
|
|
184
|
+
// record itself must show every earlier attempt closed, by a transition or by reconcile.
|
|
185
|
+
// Text in the approval is deliberately not accepted as a stop proof.
|
|
186
|
+
// Anything without a closed outcome still owns its execution: `needs_input` is a
|
|
187
|
+
// recoverable block, not proof that the previous writer stopped.
|
|
188
|
+
const stillActive = attemptsFor(record, subtaskId).filter((item) => !CLOSED_STATES.includes(item.state));
|
|
189
|
+
if (stillActive.length) {
|
|
190
|
+
return {
|
|
191
|
+
ok: false,
|
|
192
|
+
reason: 'previous_attempt_not_stopped',
|
|
193
|
+
detail: 'every earlier attempt must reach a closed outcome (completed, failed or cancelled with stop evidence) before a new attempt is registered',
|
|
194
|
+
still_active: stillActive.map((item) => `${item.subtask_id}#${item.attempt}:${item.state}`),
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
const at = nowIso(clock);
|
|
199
|
+
record.attempts = record.attempts ?? [];
|
|
200
|
+
record.attempts.push({
|
|
201
|
+
subtask_id: subtaskId,
|
|
202
|
+
attempt,
|
|
203
|
+
state: 'assigned',
|
|
204
|
+
owner_epoch: record.coordinator?.owner_epoch ?? null,
|
|
205
|
+
assignee: message.to,
|
|
206
|
+
assignment_message_id: message.message_id,
|
|
207
|
+
assignment_ref: assignmentRef,
|
|
208
|
+
assignment_digest: assignmentDigest,
|
|
209
|
+
input_revision: inputRevision ?? record.input_revision ?? null,
|
|
210
|
+
created_at: at,
|
|
211
|
+
updated_at: at,
|
|
212
|
+
native: {
|
|
213
|
+
runner: null,
|
|
214
|
+
session_id: null,
|
|
215
|
+
run_id: null,
|
|
216
|
+
mission_id: null,
|
|
217
|
+
observation: 'unknown',
|
|
218
|
+
observed_at: null,
|
|
219
|
+
evidence_ref: null,
|
|
220
|
+
},
|
|
221
|
+
approval: message.body?.approval ?? null,
|
|
222
|
+
transitions: [
|
|
223
|
+
{
|
|
224
|
+
from: null,
|
|
225
|
+
to: 'assigned',
|
|
226
|
+
at,
|
|
227
|
+
actor: message.from?.agent_id ?? 'unattributed',
|
|
228
|
+
reason: 'assignment registered',
|
|
229
|
+
evidence: null,
|
|
230
|
+
},
|
|
231
|
+
],
|
|
232
|
+
});
|
|
233
|
+
if (inputRevision)
|
|
234
|
+
record.input_revision = inputRevision;
|
|
235
|
+
return {
|
|
236
|
+
ok: true,
|
|
237
|
+
subtask_id: subtaskId,
|
|
238
|
+
attempt,
|
|
239
|
+
state: 'assigned',
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
/**
|
|
243
|
+
* Record native evidence for an attempt. `observation: confirmed` requires an evidence
|
|
244
|
+
* reference; `unknown` is a first-class observation and never means failure.
|
|
245
|
+
*/
|
|
246
|
+
export function bindNative(record, { subtaskId, attempt, runner, sessionId = null, runId = null, missionId = null, observation, evidenceRef = null, observedAt = null, clock = Date.now(), }) {
|
|
247
|
+
const target = findAttempt(record, subtaskId, attempt);
|
|
248
|
+
if (!target)
|
|
249
|
+
return { ok: false, reason: 'unknown_attempt' };
|
|
250
|
+
if (!['confirmed', 'unknown'].includes(observation))
|
|
251
|
+
return { ok: false, reason: 'invalid_observation' };
|
|
252
|
+
if (observation === 'confirmed' && !text(evidenceRef))
|
|
253
|
+
return { ok: false, reason: 'evidence_required' };
|
|
254
|
+
const at = observedAt ?? nowIso(clock);
|
|
255
|
+
target.native = {
|
|
256
|
+
runner: runner ?? null,
|
|
257
|
+
session_id: sessionId,
|
|
258
|
+
run_id: runId,
|
|
259
|
+
mission_id: missionId,
|
|
260
|
+
observation,
|
|
261
|
+
observed_at: at,
|
|
262
|
+
evidence_ref: evidenceRef,
|
|
263
|
+
// A native runner owns its own execution; the shared layer only records pointers.
|
|
264
|
+
manager: 'native',
|
|
265
|
+
};
|
|
266
|
+
target.updated_at = nowIso(clock);
|
|
267
|
+
target.transitions = target.transitions ?? [];
|
|
268
|
+
target.transitions.push({
|
|
269
|
+
from: target.state,
|
|
270
|
+
to: target.state,
|
|
271
|
+
at: target.updated_at,
|
|
272
|
+
actor: 'native-binding',
|
|
273
|
+
reason: `native observation ${observation}`,
|
|
274
|
+
evidence: evidenceRef,
|
|
275
|
+
});
|
|
276
|
+
let reconciled = null;
|
|
277
|
+
if (observation === 'confirmed') {
|
|
278
|
+
// A confirmed native observation answers the "registered but not started" question.
|
|
279
|
+
const open = (record.uncertainty ?? []).find((item) => !item.resolved_at &&
|
|
280
|
+
item.kind === 'registered_not_started' &&
|
|
281
|
+
item.subtask_id === subtaskId &&
|
|
282
|
+
item.attempt === attempt);
|
|
283
|
+
if (open) {
|
|
284
|
+
open.resolved_at = target.updated_at;
|
|
285
|
+
open.resolution = 'native execution confirmed';
|
|
286
|
+
open.resolution_evidence = evidenceRef;
|
|
287
|
+
open.resolved_by = 'native-binding';
|
|
288
|
+
open.resolution_source = 'automatic-on-confirmed-binding';
|
|
289
|
+
reconciled = open.uncertainty_id;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
return {
|
|
293
|
+
ok: true,
|
|
294
|
+
native: target.native,
|
|
295
|
+
reconciled_uncertainty: reconciled,
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
// Only these close an attempt for good. `needs_input` is a recoverable block and `running`
|
|
299
|
+
// is progress: neither may prevent the later final conclusion from being admitted.
|
|
300
|
+
const RESULT_FINAL = new Set(['completed', 'failed', 'cancelled']);
|
|
301
|
+
/**
|
|
302
|
+
* Admit a reported result. Classification:
|
|
303
|
+
* applied the attempt is active and its input revision still matches
|
|
304
|
+
* late the attempt already has a different admitted result: evidence only
|
|
305
|
+
* superseded a newer attempt or a different input revision is authoritative
|
|
306
|
+
* duplicate the same message was already admitted
|
|
307
|
+
*/
|
|
308
|
+
export function recordResult(record, { subtaskId, attempt, status, messageId, inputRevision = null, outputs = [], nativeRef = null, clock = Date.now(), evidenceRef = null, }) {
|
|
309
|
+
const target = findAttempt(record, subtaskId, attempt);
|
|
310
|
+
if (!target)
|
|
311
|
+
return { ok: false, reason: 'unknown_attempt' };
|
|
312
|
+
const at = nowIso(clock);
|
|
313
|
+
const entry = {
|
|
314
|
+
schema: 'harness-collab/result/1',
|
|
315
|
+
subtask_id: subtaskId,
|
|
316
|
+
attempt,
|
|
317
|
+
status,
|
|
318
|
+
message_id: messageId,
|
|
319
|
+
input_revision: inputRevision ?? target.input_revision ?? null,
|
|
320
|
+
outputs,
|
|
321
|
+
native_ref: nativeRef,
|
|
322
|
+
recorded_at: at,
|
|
323
|
+
evidence_ref: evidenceRef,
|
|
324
|
+
};
|
|
325
|
+
record.results = record.results ?? [];
|
|
326
|
+
const forAttempt = record.results.filter((item) => item.subtask_id === subtaskId &&
|
|
327
|
+
item.attempt === attempt &&
|
|
328
|
+
item.classification === 'applied');
|
|
329
|
+
const incomingIsTerminal = RESULT_FINAL.has(status);
|
|
330
|
+
// Progress reports and recoverable blocks are steps, not conclusions: only a previous
|
|
331
|
+
// *final* admitted result can make a final result late.
|
|
332
|
+
const existingForAttempt = incomingIsTerminal
|
|
333
|
+
? forAttempt.filter((item) => RESULT_FINAL.has(item.status))
|
|
334
|
+
: [];
|
|
335
|
+
const duplicate = record.results.find((item) => item.message_id === messageId);
|
|
336
|
+
if (duplicate) {
|
|
337
|
+
return { ok: true, classification: 'duplicate', entry: duplicate };
|
|
338
|
+
}
|
|
339
|
+
const latest = latestAttemptNumber(record, subtaskId);
|
|
340
|
+
let classification = 'applied';
|
|
341
|
+
let reason = null;
|
|
342
|
+
if (attempt < latest) {
|
|
343
|
+
classification = 'superseded';
|
|
344
|
+
reason = `attempt ${attempt} was superseded by attempt ${latest}`;
|
|
345
|
+
}
|
|
346
|
+
else if (entry.input_revision &&
|
|
347
|
+
target.input_revision &&
|
|
348
|
+
entry.input_revision !== target.input_revision) {
|
|
349
|
+
// The attempt's own pinned input is the baseline: the record-level revision changes as
|
|
350
|
+
// other subtasks are dispatched and must not invalidate a correct report.
|
|
351
|
+
classification = 'superseded';
|
|
352
|
+
reason = `result belongs to input revision ${entry.input_revision}, this attempt is pinned to ${target.input_revision}`;
|
|
353
|
+
}
|
|
354
|
+
else if (existingForAttempt.length) {
|
|
355
|
+
classification = 'late';
|
|
356
|
+
reason =
|
|
357
|
+
'the attempt already has an admitted result; this one is retained as evidence';
|
|
358
|
+
}
|
|
359
|
+
const stored = { ...entry, classification, reason };
|
|
360
|
+
record.results.push(stored);
|
|
361
|
+
if (classification !== 'applied') {
|
|
362
|
+
openUncertainty(record, {
|
|
363
|
+
kind: 'late_result',
|
|
364
|
+
subtaskId,
|
|
365
|
+
attempt,
|
|
366
|
+
detail: reason,
|
|
367
|
+
evidenceRef,
|
|
368
|
+
resolutionHint: 'compare with the admitted result; do not overwrite the newer conclusion',
|
|
369
|
+
clock,
|
|
370
|
+
});
|
|
371
|
+
return { ok: true, classification, entry: stored };
|
|
372
|
+
}
|
|
373
|
+
if (!incomingIsTerminal) {
|
|
374
|
+
// A progress report may still record a recoverable block without closing the attempt.
|
|
375
|
+
const steps = [];
|
|
376
|
+
const target2 = findAttempt(record, subtaskId, attempt);
|
|
377
|
+
if (status === 'needs_input' &&
|
|
378
|
+
target2 &&
|
|
379
|
+
!TERMINAL_STATES.includes(target2.state)) {
|
|
380
|
+
steps.push(transitionAttempt(record, {
|
|
381
|
+
subtaskId,
|
|
382
|
+
attempt,
|
|
383
|
+
to: 'needs_input',
|
|
384
|
+
actor: 'result-admission',
|
|
385
|
+
reason: `progress block ${messageId}`,
|
|
386
|
+
evidence: evidenceRef,
|
|
387
|
+
clock,
|
|
388
|
+
}));
|
|
389
|
+
}
|
|
390
|
+
else if (status === 'running' &&
|
|
391
|
+
target2 &&
|
|
392
|
+
['assigned', 'accepted'].includes(target2.state)) {
|
|
393
|
+
steps.push(transitionAttempt(record, {
|
|
394
|
+
subtaskId,
|
|
395
|
+
attempt,
|
|
396
|
+
to: 'running',
|
|
397
|
+
actor: 'result-admission',
|
|
398
|
+
reason: `progress report ${messageId}`,
|
|
399
|
+
evidence: evidenceRef,
|
|
400
|
+
clock,
|
|
401
|
+
}));
|
|
402
|
+
}
|
|
403
|
+
return {
|
|
404
|
+
ok: true,
|
|
405
|
+
classification,
|
|
406
|
+
entry: stored,
|
|
407
|
+
note: 'progress report retained without closing the attempt',
|
|
408
|
+
transitions: steps,
|
|
409
|
+
};
|
|
410
|
+
}
|
|
411
|
+
// A terminal report means the attempt executed: walk it through `running` so the
|
|
412
|
+
// recorded history matches reality instead of skipping a state.
|
|
413
|
+
const steps = [];
|
|
414
|
+
// `needs_input` is a recoverable block: a final result may pass through it, but the
|
|
415
|
+
// approval guard in transitionAttempt still refuses to leave an unanswered approval block.
|
|
416
|
+
if (['assigned', 'accepted', 'needs_input'].includes(target.state)) {
|
|
417
|
+
steps.push(transitionAttempt(record, {
|
|
418
|
+
subtaskId,
|
|
419
|
+
attempt,
|
|
420
|
+
to: 'running',
|
|
421
|
+
actor: 'result-admission',
|
|
422
|
+
reason: `report ${messageId} implies execution`,
|
|
423
|
+
evidence: evidenceRef,
|
|
424
|
+
clock,
|
|
425
|
+
}));
|
|
426
|
+
}
|
|
427
|
+
steps.push(transitionAttempt(record, {
|
|
428
|
+
subtaskId,
|
|
429
|
+
attempt,
|
|
430
|
+
to: status,
|
|
431
|
+
actor: 'result-admission',
|
|
432
|
+
reason: `report ${messageId}`,
|
|
433
|
+
evidence: evidenceRef,
|
|
434
|
+
clock,
|
|
435
|
+
}));
|
|
436
|
+
const finalTransition = steps.at(-1);
|
|
437
|
+
if (finalTransition &&
|
|
438
|
+
finalTransition.ok === false &&
|
|
439
|
+
finalTransition.reason !== 'already_in_state') {
|
|
440
|
+
// The conclusion could not be recorded, so it must not count as this attempt's admitted
|
|
441
|
+
// result: otherwise a later, legitimate completion would be judged `late`.
|
|
442
|
+
stored.classification = 'withheld';
|
|
443
|
+
stored.reason = `final transition refused: ${finalTransition.reason}`;
|
|
444
|
+
openUncertainty(record, {
|
|
445
|
+
kind: 'late_result',
|
|
446
|
+
subtaskId,
|
|
447
|
+
attempt,
|
|
448
|
+
detail: `a final report could not be admitted (${finalTransition.reason}); it is retained as evidence only`,
|
|
449
|
+
evidenceRef,
|
|
450
|
+
resolutionHint: 'resolve the blocking state (for example an approval block) and report again',
|
|
451
|
+
clock,
|
|
452
|
+
});
|
|
453
|
+
return {
|
|
454
|
+
ok: true,
|
|
455
|
+
classification: 'withheld',
|
|
456
|
+
entry: stored,
|
|
457
|
+
transitions: steps,
|
|
458
|
+
transition: finalTransition,
|
|
459
|
+
};
|
|
460
|
+
}
|
|
461
|
+
return {
|
|
462
|
+
ok: true,
|
|
463
|
+
classification,
|
|
464
|
+
entry: stored,
|
|
465
|
+
transition: finalTransition,
|
|
466
|
+
transitions: steps,
|
|
467
|
+
};
|
|
468
|
+
}
|
|
469
|
+
export function writeResultEvidence(taskRoot, entry) {
|
|
470
|
+
fs.mkdirSync(resultsDir(taskRoot), { recursive: true, mode: 0o700 });
|
|
471
|
+
const name = `${entry.subtask_id}-a${entry.attempt}-${entry.message_id ?? createNonce()}.json`;
|
|
472
|
+
const file = path.join(resultsDir(taskRoot), name.replace(/[^A-Za-z0-9._-]/g, '_'));
|
|
473
|
+
if (!fs.existsSync(file)) {
|
|
474
|
+
fs.writeFileSync(file, `${JSON.stringify(entry, null, 2)}\n`, {
|
|
475
|
+
flag: 'wx',
|
|
476
|
+
mode: 0o600,
|
|
477
|
+
});
|
|
478
|
+
}
|
|
479
|
+
return file;
|
|
480
|
+
}
|
|
481
|
+
export function openUncertainty(record, { kind, subtaskId = null, attempt = null, detail = null, evidenceRef = null, resolutionHint = null, clock = Date.now(), }) {
|
|
482
|
+
if (!UNCERTAINTY_KINDS.includes(kind))
|
|
483
|
+
return { ok: false, reason: 'unknown_uncertainty_kind', kind };
|
|
484
|
+
const existing = (record.uncertainty ?? []).find((item) => !item.resolved_at &&
|
|
485
|
+
item.kind === kind &&
|
|
486
|
+
item.subtask_id === subtaskId &&
|
|
487
|
+
item.attempt === attempt &&
|
|
488
|
+
item.detail === detail);
|
|
489
|
+
if (existing)
|
|
490
|
+
return {
|
|
491
|
+
ok: true,
|
|
492
|
+
uncertainty_id: existing.uncertainty_id,
|
|
493
|
+
duplicate: true,
|
|
494
|
+
};
|
|
495
|
+
const entry = {
|
|
496
|
+
uncertainty_id: `unc-${createNonce()}`,
|
|
497
|
+
kind,
|
|
498
|
+
subtask_id: subtaskId,
|
|
499
|
+
attempt,
|
|
500
|
+
detail,
|
|
501
|
+
evidence_ref: evidenceRef,
|
|
502
|
+
resolution_hint: resolutionHint,
|
|
503
|
+
opened_at: nowIso(clock),
|
|
504
|
+
resolved_at: null,
|
|
505
|
+
resolution: null,
|
|
506
|
+
resolution_evidence: null,
|
|
507
|
+
// Never auto-restart: the native runner owns execution, the shared layer only reports.
|
|
508
|
+
auto_restart: false,
|
|
509
|
+
};
|
|
510
|
+
record.uncertainty = record.uncertainty ?? [];
|
|
511
|
+
record.uncertainty.push(entry);
|
|
512
|
+
return { ok: true, uncertainty_id: entry.uncertainty_id };
|
|
513
|
+
}
|
|
514
|
+
export function resolveUncertainty(record, { uncertaintyId, resolution, resolutionEvidence, actor, clock = Date.now(), }) {
|
|
515
|
+
const entry = (record.uncertainty ?? []).find((item) => item.uncertainty_id === uncertaintyId);
|
|
516
|
+
if (!entry)
|
|
517
|
+
return { ok: false, reason: 'unknown_uncertainty' };
|
|
518
|
+
if (entry.resolved_at)
|
|
519
|
+
return {
|
|
520
|
+
ok: false,
|
|
521
|
+
reason: 'already_resolved',
|
|
522
|
+
resolved_at: entry.resolved_at,
|
|
523
|
+
};
|
|
524
|
+
if (!text(resolution))
|
|
525
|
+
return { ok: false, reason: 'resolution_required' };
|
|
526
|
+
entry.resolved_at = nowIso(clock);
|
|
527
|
+
entry.resolution = resolution;
|
|
528
|
+
entry.resolution_evidence = resolutionEvidence ?? null;
|
|
529
|
+
entry.resolved_by = actor ?? 'unattributed';
|
|
530
|
+
return { ok: true, uncertainty_id: uncertaintyId, resolution };
|
|
531
|
+
}
|
|
532
|
+
export function recordDigest(record) {
|
|
533
|
+
return digestText(canonicalJson(record));
|
|
534
|
+
}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import type { TaskRecord, Message, MessageEffect, Mutation, Failure } from './types.js';
|
|
2
|
+
import { bindNative } from './lifecycle.js';
|
|
3
|
+
/**
|
|
4
|
+
* Pure-ish apply function for `transact`: returns the outcome object or `{ok:false,...}`.
|
|
5
|
+
*/
|
|
6
|
+
export declare function applyMessage(record: TaskRecord, message: Message, { clock, nativeDir: _nativeDirectory, }?: {
|
|
7
|
+
clock?: number;
|
|
8
|
+
nativeDir?: string | null;
|
|
9
|
+
}): Mutation<MessageEffect> | Failure;
|
|
10
|
+
/**
|
|
11
|
+
* Register and apply one message. Returns the registration result plus the applied
|
|
12
|
+
* outcome; a duplicate registration produces no second side effect.
|
|
13
|
+
*/
|
|
14
|
+
export declare function submitMessage(taskRoot: string, message: Message, { clock, expectRevision, waitMs, }?: {
|
|
15
|
+
clock?: number;
|
|
16
|
+
expectRevision?: number;
|
|
17
|
+
waitMs?: number;
|
|
18
|
+
}): {
|
|
19
|
+
duplicate?: never;
|
|
20
|
+
ok: false;
|
|
21
|
+
reason: string;
|
|
22
|
+
registration: {
|
|
23
|
+
status: string;
|
|
24
|
+
message_id: string;
|
|
25
|
+
stored_digest: string;
|
|
26
|
+
actual_digest: string;
|
|
27
|
+
content_digest?: never;
|
|
28
|
+
file?: never;
|
|
29
|
+
} | {
|
|
30
|
+
status: string;
|
|
31
|
+
message_id: string;
|
|
32
|
+
content_digest: string;
|
|
33
|
+
file: string;
|
|
34
|
+
stored_digest?: never;
|
|
35
|
+
actual_digest?: never;
|
|
36
|
+
} | {
|
|
37
|
+
content_digest?: never;
|
|
38
|
+
status: string;
|
|
39
|
+
message_id: string;
|
|
40
|
+
stored_digest: string | null;
|
|
41
|
+
actual_digest: string;
|
|
42
|
+
file: string;
|
|
43
|
+
};
|
|
44
|
+
recorded?: never;
|
|
45
|
+
state_revision?: never;
|
|
46
|
+
applied?: never;
|
|
47
|
+
} | {
|
|
48
|
+
reason?: never;
|
|
49
|
+
ok: true;
|
|
50
|
+
registration: {
|
|
51
|
+
status: string;
|
|
52
|
+
message_id: string;
|
|
53
|
+
stored_digest: string;
|
|
54
|
+
actual_digest: string;
|
|
55
|
+
content_digest?: never;
|
|
56
|
+
file?: never;
|
|
57
|
+
} | {
|
|
58
|
+
status: string;
|
|
59
|
+
message_id: string;
|
|
60
|
+
content_digest: string;
|
|
61
|
+
file: string;
|
|
62
|
+
stored_digest?: never;
|
|
63
|
+
actual_digest?: never;
|
|
64
|
+
} | {
|
|
65
|
+
content_digest?: never;
|
|
66
|
+
status: string;
|
|
67
|
+
message_id: string;
|
|
68
|
+
stored_digest: string | null;
|
|
69
|
+
actual_digest: string;
|
|
70
|
+
file: string;
|
|
71
|
+
};
|
|
72
|
+
duplicate: boolean;
|
|
73
|
+
applied: {
|
|
74
|
+
[key: string]: unknown;
|
|
75
|
+
ok: false;
|
|
76
|
+
reason: string;
|
|
77
|
+
};
|
|
78
|
+
recorded?: never;
|
|
79
|
+
state_revision?: never;
|
|
80
|
+
} | {
|
|
81
|
+
duplicate?: never;
|
|
82
|
+
ok: false;
|
|
83
|
+
reason: string;
|
|
84
|
+
registration: {
|
|
85
|
+
status: string;
|
|
86
|
+
message_id: string;
|
|
87
|
+
stored_digest: string;
|
|
88
|
+
actual_digest: string;
|
|
89
|
+
content_digest?: never;
|
|
90
|
+
file?: never;
|
|
91
|
+
} | {
|
|
92
|
+
status: string;
|
|
93
|
+
message_id: string;
|
|
94
|
+
content_digest: string;
|
|
95
|
+
file: string;
|
|
96
|
+
stored_digest?: never;
|
|
97
|
+
actual_digest?: never;
|
|
98
|
+
} | {
|
|
99
|
+
content_digest?: never;
|
|
100
|
+
status: string;
|
|
101
|
+
message_id: string;
|
|
102
|
+
stored_digest: string | null;
|
|
103
|
+
actual_digest: string;
|
|
104
|
+
file: string;
|
|
105
|
+
};
|
|
106
|
+
applied: {
|
|
107
|
+
[key: string]: unknown;
|
|
108
|
+
ok: false;
|
|
109
|
+
reason: string;
|
|
110
|
+
};
|
|
111
|
+
recorded: boolean;
|
|
112
|
+
state_revision?: never;
|
|
113
|
+
} | {
|
|
114
|
+
reason?: never;
|
|
115
|
+
duplicate?: never;
|
|
116
|
+
recorded?: never;
|
|
117
|
+
ok: true;
|
|
118
|
+
registration: {
|
|
119
|
+
status: string;
|
|
120
|
+
message_id: string;
|
|
121
|
+
stored_digest: string;
|
|
122
|
+
actual_digest: string;
|
|
123
|
+
content_digest?: never;
|
|
124
|
+
file?: never;
|
|
125
|
+
} | {
|
|
126
|
+
status: string;
|
|
127
|
+
message_id: string;
|
|
128
|
+
content_digest: string;
|
|
129
|
+
file: string;
|
|
130
|
+
stored_digest?: never;
|
|
131
|
+
actual_digest?: never;
|
|
132
|
+
} | {
|
|
133
|
+
content_digest?: never;
|
|
134
|
+
status: string;
|
|
135
|
+
message_id: string;
|
|
136
|
+
stored_digest: string | null;
|
|
137
|
+
actual_digest: string;
|
|
138
|
+
file: string;
|
|
139
|
+
};
|
|
140
|
+
state_revision: number;
|
|
141
|
+
applied: {
|
|
142
|
+
ok: true;
|
|
143
|
+
state_revision: number;
|
|
144
|
+
result: MessageEffect | null;
|
|
145
|
+
op: string;
|
|
146
|
+
};
|
|
147
|
+
};
|
|
148
|
+
export { bindNative };
|