harnery 0.43.0 → 0.45.0
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 +3 -3
- package/dist/commander.js +1 -0
- package/dist/commands/agents.d.ts.map +1 -1
- package/dist/commands/agents.js +41 -48
- package/dist/commands/browse.d.ts.map +1 -1
- package/dist/commands/browse.js +38 -11
- package/dist/commands/diagnostics.d.ts.map +1 -1
- package/dist/commands/diagnostics.js +10 -0
- package/dist/commands/doctor.d.ts +12 -0
- package/dist/commands/doctor.d.ts.map +1 -1
- package/dist/commands/doctor.js +22 -7
- package/dist/commands/files.d.ts +2 -2
- package/dist/commands/files.d.ts.map +1 -1
- package/dist/commands/files.js +7 -4
- package/dist/commands/governor.js +22 -10
- package/dist/commands/inbox.d.ts +1 -1
- package/dist/commands/inbox.d.ts.map +1 -1
- package/dist/commands/inbox.js +55 -2
- package/dist/commands/rm.d.ts.map +1 -1
- package/dist/commands/rm.js +28 -4
- package/dist/commands/servers.d.ts.map +1 -1
- package/dist/commands/servers.js +26 -17
- package/dist/commands/supervisor.d.ts.map +1 -1
- package/dist/commands/supervisor.js +7 -2
- package/dist/commands/toc.d.ts +14 -0
- package/dist/commands/toc.d.ts.map +1 -1
- package/dist/commands/toc.js +7 -4
- package/dist/commands/tunnel.d.ts +15 -0
- package/dist/commands/tunnel.d.ts.map +1 -1
- package/dist/commands/tunnel.js +453 -153
- package/dist/commands/turn-rollback.d.ts +5 -0
- package/dist/commands/turn-rollback.d.ts.map +1 -0
- package/dist/commands/turn-rollback.js +76 -0
- package/dist/commands/workflow.d.ts.map +1 -1
- package/dist/commands/workflow.js +68 -30
- package/dist/core/agents/coord-client.d.ts.map +1 -1
- package/dist/core/agents/coord-client.js +18 -0
- package/dist/core/agents/live-authority-v3.d.ts +1 -0
- package/dist/core/agents/live-authority-v3.d.ts.map +1 -1
- package/dist/core/agents/live-authority-v3.js +11 -0
- package/dist/core/agents/live-lifecycle-v3.d.ts +4 -2
- package/dist/core/agents/live-lifecycle-v3.d.ts.map +1 -1
- package/dist/core/agents/live-lifecycle-v3.js +28 -7
- package/dist/core/agents/mailbox.d.ts +9 -8
- package/dist/core/agents/mailbox.d.ts.map +1 -1
- package/dist/core/agents/mailbox.js +24 -17
- package/dist/core/agents/render/prompt-context.d.ts +2 -0
- package/dist/core/agents/render/prompt-context.d.ts.map +1 -1
- package/dist/core/agents/render/prompt-context.js +8 -12
- package/dist/core/agents/render/session-context.d.ts +2 -8
- package/dist/core/agents/render/session-context.d.ts.map +1 -1
- package/dist/core/agents/render/session-context.js +7 -30
- package/dist/core/agents/rules/stop-hook.js +3 -3
- package/dist/core/agents/state/heartbeat-writer.d.ts.map +1 -1
- package/dist/core/agents/state/heartbeat-writer.js +25 -14
- package/dist/core/agents/state/live-coordination-writer.d.ts.map +1 -1
- package/dist/core/agents/state/live-coordination-writer.js +5 -0
- package/dist/core/agents/state/pidmap.d.ts.map +1 -1
- package/dist/core/agents/state/pidmap.js +5 -0
- package/dist/core/agents/state/proc-start.d.ts +19 -1
- package/dist/core/agents/state/proc-start.d.ts.map +1 -1
- package/dist/core/agents/state/proc-start.js +46 -1
- package/dist/core/agents/state/process-handle.d.ts +26 -0
- package/dist/core/agents/state/process-handle.d.ts.map +1 -0
- package/dist/core/agents/state/process-handle.js +121 -0
- package/dist/core/agents/state/stale-sweep.d.ts.map +1 -1
- package/dist/core/agents/state/stale-sweep.js +180 -70
- package/dist/core/agents/state/unchanged-file.d.ts +44 -0
- package/dist/core/agents/state/unchanged-file.d.ts.map +1 -0
- package/dist/core/agents/state/unchanged-file.js +390 -0
- package/dist/core/artifacts/delivery-card.d.ts.map +1 -1
- package/dist/core/artifacts/delivery-card.js +2 -1
- package/dist/core/context/index.d.ts +22 -10
- package/dist/core/context/index.d.ts.map +1 -1
- package/dist/core/context/index.js +381 -119
- package/dist/core/context/observation.d.ts +26 -0
- package/dist/core/context/observation.d.ts.map +1 -0
- package/dist/core/context/observation.js +11 -0
- package/dist/core/diagnostics/bundle.d.ts.map +1 -1
- package/dist/core/diagnostics/bundle.js +25 -14
- package/dist/core/diagnostics/path-identity.d.ts +3 -0
- package/dist/core/diagnostics/path-identity.d.ts.map +1 -0
- package/dist/core/diagnostics/path-identity.js +12 -0
- package/dist/core/diagnostics/sanitize.d.ts.map +1 -1
- package/dist/core/diagnostics/sanitize.js +4 -3
- package/dist/core/events/v3/authority-outbox.d.ts.map +1 -1
- package/dist/core/events/v3/authority-outbox.js +29 -0
- package/dist/core/events/v3/generation-custody.d.ts +5 -0
- package/dist/core/events/v3/generation-custody.d.ts.map +1 -0
- package/dist/core/events/v3/generation-custody.js +50 -0
- package/dist/core/events/v3/producers/coordination-recorder.d.ts +1 -0
- package/dist/core/events/v3/producers/coordination-recorder.d.ts.map +1 -1
- package/dist/core/events/v3/producers/coordination-recorder.js +7 -0
- package/dist/core/events/v3/producers/recorder.d.ts.map +1 -1
- package/dist/core/events/v3/producers/recorder.js +77 -64
- package/dist/core/governor/planning.d.ts.map +1 -1
- package/dist/core/governor/planning.js +14 -1
- package/dist/core/governor/service-read.d.ts +10 -2
- package/dist/core/governor/service-read.d.ts.map +1 -1
- package/dist/core/governor/service-read.js +105 -22
- package/dist/core/governor/service.d.ts +20 -2
- package/dist/core/governor/service.d.ts.map +1 -1
- package/dist/core/governor/service.js +72 -47
- package/dist/core/hooks/cli.js +33 -5
- package/dist/core/inbox/contract.d.ts +2 -1
- package/dist/core/inbox/contract.d.ts.map +1 -1
- package/dist/core/inbox/index.d.ts +2 -0
- package/dist/core/inbox/index.d.ts.map +1 -1
- package/dist/core/inbox/index.js +2 -0
- package/dist/core/inbox/peer-messages.d.ts +37 -0
- package/dist/core/inbox/peer-messages.d.ts.map +1 -0
- package/dist/core/inbox/peer-messages.js +72 -0
- package/dist/core/inbox/service.d.ts +8 -1
- package/dist/core/inbox/service.d.ts.map +1 -1
- package/dist/core/inbox/service.js +67 -21
- package/dist/core/inbox/targets.d.ts +22 -0
- package/dist/core/inbox/targets.d.ts.map +1 -0
- package/dist/core/inbox/targets.js +108 -0
- package/dist/core/inbox/watcher.js +7 -3
- package/dist/core/journal/index.d.ts.map +1 -1
- package/dist/core/journal/index.js +17 -2
- package/dist/core/resources/contract.d.ts +3 -0
- package/dist/core/resources/contract.d.ts.map +1 -1
- package/dist/core/resources/service-status.d.ts +14 -1
- package/dist/core/resources/service-status.d.ts.map +1 -1
- package/dist/core/resources/service-status.js +23 -13
- package/dist/core/resources/service.d.ts +7 -13
- package/dist/core/resources/service.d.ts.map +1 -1
- package/dist/core/resources/service.js +34 -91
- package/dist/core/resources/status.d.ts +2 -5
- package/dist/core/resources/status.d.ts.map +1 -1
- package/dist/core/resources/status.js +36 -7
- package/dist/core/semantic/service.d.ts.map +1 -1
- package/dist/core/semantic/service.js +2 -9
- package/dist/core/servers/index.d.ts +34 -22
- package/dist/core/servers/index.d.ts.map +1 -1
- package/dist/core/servers/index.js +297 -144
- package/dist/core/servers/tunnels.d.ts +7 -7
- package/dist/core/servers/tunnels.d.ts.map +1 -1
- package/dist/core/servers/tunnels.js +47 -10
- package/dist/core/supervisor/contract.d.ts +12 -0
- package/dist/core/supervisor/contract.d.ts.map +1 -1
- package/dist/core/supervisor/explanations.d.ts.map +1 -1
- package/dist/core/supervisor/explanations.js +2 -1
- package/dist/core/supervisor/findings.d.ts.map +1 -1
- package/dist/core/supervisor/findings.js +54 -9
- package/dist/core/supervisor/service.d.ts +7 -1
- package/dist/core/supervisor/service.d.ts.map +1 -1
- package/dist/core/supervisor/service.js +59 -29
- package/dist/core/supervisor/services.d.ts.map +1 -1
- package/dist/core/supervisor/services.js +22 -1
- package/dist/core/supervisor/status.d.ts +17 -2
- package/dist/core/supervisor/status.d.ts.map +1 -1
- package/dist/core/supervisor/status.js +70 -12
- package/dist/core/supervisor/timeline.d.ts.map +1 -1
- package/dist/core/supervisor/timeline.js +2 -1
- package/dist/core/turn-rollback/content.d.ts +53 -0
- package/dist/core/turn-rollback/content.d.ts.map +1 -0
- package/dist/core/turn-rollback/content.js +396 -0
- package/dist/core/turn-rollback/index.d.ts +72 -0
- package/dist/core/turn-rollback/index.d.ts.map +1 -0
- package/dist/core/turn-rollback/index.js +443 -0
- package/dist/core/work/state.js +7 -7
- package/dist/core/workflow/engine.d.ts.map +1 -1
- package/dist/core/workflow/engine.js +105 -32
- package/dist/core/workflow/failure-evidence.d.ts +16 -0
- package/dist/core/workflow/failure-evidence.d.ts.map +1 -0
- package/dist/core/workflow/failure-evidence.js +114 -0
- package/dist/core/workflow/index.d.ts +5 -3
- package/dist/core/workflow/index.d.ts.map +1 -1
- package/dist/core/workflow/index.js +3 -2
- package/dist/core/workflow/live-session-v3.d.ts +6 -0
- package/dist/core/workflow/live-session-v3.d.ts.map +1 -1
- package/dist/core/workflow/live-session-v3.js +126 -5
- package/dist/core/workflow/proof.d.ts +5 -5
- package/dist/core/workflow/proof.d.ts.map +1 -1
- package/dist/core/workflow/proof.js +115 -20
- package/dist/core/workflow/recovery-audit.d.ts +30 -0
- package/dist/core/workflow/recovery-audit.d.ts.map +1 -0
- package/dist/core/workflow/recovery-audit.js +116 -0
- package/dist/core/workflow/run-state.d.ts +20 -1
- package/dist/core/workflow/run-state.d.ts.map +1 -1
- package/dist/core/workflow/run-state.js +141 -32
- package/dist/core/workflow/spawn-claude.d.ts.map +1 -1
- package/dist/core/workflow/spawn-claude.js +19 -0
- package/dist/core/workflow/spawn-codex.d.ts.map +1 -1
- package/dist/core/workflow/spawn-codex.js +13 -0
- package/dist/core/workflow/spawn-cursor.d.ts.map +1 -1
- package/dist/core/workflow/spawn-cursor.js +19 -0
- package/dist/core/workflow/spawn-opencode.d.ts.map +1 -1
- package/dist/core/workflow/spawn-opencode.js +14 -0
- package/dist/core/workflow/types.d.ts +64 -17
- package/dist/core/workflow/types.d.ts.map +1 -1
- package/dist/core/workflow/workspaces/index.d.ts +0 -1
- package/dist/core/workflow/workspaces/index.d.ts.map +1 -1
- package/dist/core/workflow/workspaces/index.js +0 -1
- package/dist/core/workflow/workspaces/leases.d.ts +3 -1
- package/dist/core/workflow/workspaces/leases.d.ts.map +1 -1
- package/dist/core/workflow/workspaces/leases.js +30 -16
- package/dist/core/workflow/workspaces/local-git.d.ts +6 -2
- package/dist/core/workflow/workspaces/local-git.d.ts.map +1 -1
- package/dist/core/workflow/workspaces/local-git.js +328 -8
- package/dist/core/workflow/workspaces/ownership.d.ts +11 -0
- package/dist/core/workflow/workspaces/ownership.d.ts.map +1 -0
- package/dist/core/workflow/workspaces/ownership.js +194 -0
- package/dist/core/workflow/workspaces/reclaim.d.ts +40 -0
- package/dist/core/workflow/workspaces/reclaim.d.ts.map +1 -1
- package/dist/core/workflow/workspaces/reclaim.js +14 -0
- package/dist/core/workflow/workspaces/recovery.d.ts +2 -0
- package/dist/core/workflow/workspaces/recovery.d.ts.map +1 -1
- package/dist/core/workflow/workspaces/recovery.js +42 -10
- package/dist/core/workflow/workspaces/state.d.ts +1 -0
- package/dist/core/workflow/workspaces/state.d.ts.map +1 -1
- package/dist/core/workflow/workspaces/state.js +21 -1
- package/dist/lib/browser/windows-chrome.d.ts +100 -12
- package/dist/lib/browser/windows-chrome.d.ts.map +1 -1
- package/dist/lib/browser/windows-chrome.js +262 -73
- package/dist/lib/docs-links.d.ts.map +1 -1
- package/dist/lib/docs-links.js +45 -2
- package/dist/lib/guarded-remove.d.ts +7 -0
- package/dist/lib/guarded-remove.d.ts.map +1 -1
- package/dist/lib/guarded-remove.js +106 -12
- package/dist/lib/identities/assume.d.ts.map +1 -1
- package/dist/lib/identities/assume.js +4 -0
- package/dist/lib/tunnel/state.d.ts +13 -6
- package/dist/lib/tunnel/state.d.ts.map +1 -1
- package/dist/lib/tunnel/state.js +58 -12
- package/dist/lib/zip/index.d.ts +41 -0
- package/dist/lib/zip/index.d.ts.map +1 -0
- package/dist/lib/zip/index.js +398 -0
- package/package.json +12 -1
- package/src/commander.ts +10 -0
- package/src/commands/agents.ts +54 -63
- package/src/commands/browse.ts +50 -14
- package/src/commands/diagnostics.ts +10 -0
- package/src/commands/doctor.ts +26 -7
- package/src/commands/files.ts +13 -5
- package/src/commands/governor.ts +23 -10
- package/src/commands/inbox.ts +52 -2
- package/src/commands/rm.ts +43 -4
- package/src/commands/servers.ts +27 -26
- package/src/commands/supervisor.ts +7 -2
- package/src/commands/toc.ts +14 -6
- package/src/commands/tunnel.ts +533 -166
- package/src/commands/turn-rollback.ts +111 -0
- package/src/commands/workflow.ts +89 -30
- package/src/core/agents/coord-client.ts +20 -0
- package/src/core/agents/live-authority-v3.ts +17 -0
- package/src/core/agents/live-lifecycle-v3.ts +52 -14
- package/src/core/agents/mailbox.ts +26 -17
- package/src/core/agents/render/prompt-context.ts +10 -12
- package/src/core/agents/render/session-context.ts +9 -32
- package/src/core/agents/rules/stop-hook.ts +3 -3
- package/src/core/agents/state/heartbeat-writer.ts +29 -12
- package/src/core/agents/state/live-coordination-writer.ts +29 -0
- package/src/core/agents/state/pidmap.ts +13 -0
- package/src/core/agents/state/proc-start.ts +62 -2
- package/src/core/agents/state/process-handle.ts +165 -0
- package/src/core/agents/state/stale-sweep.ts +247 -82
- package/src/core/agents/state/unchanged-file.ts +456 -0
- package/src/core/artifacts/delivery-card.ts +7 -1
- package/src/core/context/index.ts +446 -136
- package/src/core/context/observation.ts +29 -0
- package/src/core/diagnostics/bundle.ts +23 -18
- package/src/core/diagnostics/path-identity.ts +15 -0
- package/src/core/diagnostics/sanitize.ts +4 -3
- package/src/core/events/v3/authority-outbox.ts +56 -0
- package/src/core/events/v3/generation-custody.ts +59 -0
- package/src/core/events/v3/producers/coordination-recorder.ts +15 -0
- package/src/core/events/v3/producers/recorder.ts +94 -68
- package/src/core/governor/planning.ts +20 -8
- package/src/core/governor/service-read.ts +128 -20
- package/src/core/governor/service.ts +119 -42
- package/src/core/hooks/cli.ts +33 -5
- package/src/core/inbox/contract.ts +2 -1
- package/src/core/inbox/index.ts +2 -0
- package/src/core/inbox/peer-messages.ts +94 -0
- package/src/core/inbox/service.ts +88 -22
- package/src/core/inbox/targets.ts +148 -0
- package/src/core/inbox/watcher.ts +8 -9
- package/src/core/journal/index.ts +19 -2
- package/src/core/resources/contract.ts +4 -0
- package/src/core/resources/service-status.ts +35 -12
- package/src/core/resources/service.ts +57 -95
- package/src/core/resources/status.ts +57 -8
- package/src/core/semantic/service.ts +2 -8
- package/src/core/servers/index.ts +376 -141
- package/src/core/servers/tunnels.ts +57 -13
- package/src/core/supervisor/contract.ts +13 -0
- package/src/core/supervisor/explanations.ts +4 -1
- package/src/core/supervisor/findings.ts +68 -10
- package/src/core/supervisor/service.ts +87 -26
- package/src/core/supervisor/services.ts +26 -8
- package/src/core/supervisor/status.ts +89 -12
- package/src/core/supervisor/timeline.ts +2 -1
- package/src/core/turn-rollback/content.ts +499 -0
- package/src/core/turn-rollback/index.ts +580 -0
- package/src/core/work/state.ts +7 -7
- package/src/core/workflow/engine.ts +131 -49
- package/src/core/workflow/failure-evidence.ts +141 -0
- package/src/core/workflow/index.ts +12 -2
- package/src/core/workflow/live-session-v3.ts +146 -5
- package/src/core/workflow/proof.ts +134 -26
- package/src/core/workflow/recovery-audit.ts +181 -0
- package/src/core/workflow/run-state.ts +165 -34
- package/src/core/workflow/spawn-claude.ts +18 -0
- package/src/core/workflow/spawn-codex.ts +13 -0
- package/src/core/workflow/spawn-cursor.ts +18 -0
- package/src/core/workflow/spawn-opencode.ts +14 -0
- package/src/core/workflow/types.ts +51 -17
- package/src/core/workflow/workspaces/index.ts +0 -1
- package/src/core/workflow/workspaces/leases.ts +48 -17
- package/src/core/workflow/workspaces/local-git.ts +388 -7
- package/src/core/workflow/workspaces/ownership.ts +232 -0
- package/src/core/workflow/workspaces/reclaim.ts +62 -0
- package/src/core/workflow/workspaces/recovery.ts +45 -10
- package/src/core/workflow/workspaces/state.ts +20 -0
- package/src/lib/browser/windows-chrome.ts +346 -86
- package/src/lib/docs-links.ts +49 -2
- package/src/lib/guarded-remove.ts +132 -14
- package/src/lib/identities/assume.ts +4 -0
- package/src/lib/tunnel/state.ts +70 -15
- package/src/lib/zip/index.ts +483 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import { existsSync, readFileSync, statSync } from "node:fs";
|
|
3
|
-
import { join } from "node:path";
|
|
4
|
-
import type
|
|
3
|
+
import { isAbsolute, join } from "node:path";
|
|
4
|
+
import { observationsComparable, type RepoSnapshot } from "../context/observation.ts";
|
|
5
5
|
import { DIAGNOSTIC_ADVICE_SCHEMA_VERSION } from "../diagnostics/contract.ts";
|
|
6
6
|
import type {
|
|
7
7
|
NormalizedPolicy,
|
|
@@ -12,6 +12,7 @@ import type {
|
|
|
12
12
|
import { policyDigest } from "../policy/index.ts";
|
|
13
13
|
import { isCanonicalWorkflowAttemptContext } from "./attempt-context.ts";
|
|
14
14
|
import { stableDigest, writeImmutableJson } from "./durable-record.ts";
|
|
15
|
+
import { failureEvidenceAllowsUncharged, isSpawnFailureEvidence } from "./failure-evidence.ts";
|
|
15
16
|
import { readWorkflowRunManifest } from "./run-state.ts";
|
|
16
17
|
import type {
|
|
17
18
|
AcceptanceCriterion,
|
|
@@ -242,17 +243,25 @@ export function digestResult(value: unknown, kind?: "text" | "json"): ResultDige
|
|
|
242
243
|
* attempt was informative about the work — charge it. This also keeps a
|
|
243
244
|
* resumed run whose earlier segment did real work from being written off by
|
|
244
245
|
* a later environment failure.
|
|
245
|
-
* 3.
|
|
246
|
-
*
|
|
247
|
-
* hard stop.
|
|
246
|
+
* 3. Every participant must have failed with positive no-effect evidence,
|
|
247
|
+
* observed absent output and no refusal. A class alone is insufficient.
|
|
248
|
+
* Among those eligible failures, environment is the hard stop.
|
|
248
249
|
* 4. Anything else is undefined ⇒ a charged work failure, exactly as today.
|
|
249
250
|
*/
|
|
250
251
|
export function deriveRunFailureClass(
|
|
251
252
|
status: "succeeded" | "failed",
|
|
252
|
-
agents: readonly Pick<WorkflowAgentProof, "status" | "class">[],
|
|
253
|
+
agents: readonly Pick<WorkflowAgentProof, "status" | "class" | "failure_evidence">[],
|
|
253
254
|
): SpawnFailureClass | undefined {
|
|
254
255
|
if (status !== "failed") return undefined;
|
|
255
|
-
if (
|
|
256
|
+
if (
|
|
257
|
+
agents.length === 0 ||
|
|
258
|
+
agents.some(
|
|
259
|
+
(agent) =>
|
|
260
|
+
agent.status !== "failed" ||
|
|
261
|
+
(agent.class !== "environment" && agent.class !== "upstream") ||
|
|
262
|
+
!failureEvidenceAllowsUncharged(agent.failure_evidence),
|
|
263
|
+
)
|
|
264
|
+
) {
|
|
256
265
|
return undefined;
|
|
257
266
|
}
|
|
258
267
|
const failed = agents.filter((agent) => agent.status === "failed");
|
|
@@ -385,6 +394,13 @@ export function readWorkflowProof(coordRoot: string, runId: string): WorkflowPro
|
|
|
385
394
|
proof.run.class !== "environment" &&
|
|
386
395
|
proof.run.class !== "upstream" &&
|
|
387
396
|
proof.run.class !== "decision") ||
|
|
397
|
+
!Array.isArray(proof.agents) ||
|
|
398
|
+
proof.agents.some(
|
|
399
|
+
(agent) =>
|
|
400
|
+
!agent ||
|
|
401
|
+
typeof agent !== "object" ||
|
|
402
|
+
(agent.failure_evidence !== undefined && !isSpawnFailureEvidence(agent.failure_evidence)),
|
|
403
|
+
) ||
|
|
388
404
|
(proof.run.work_context !== undefined &&
|
|
389
405
|
(!proof.run.work_item_id ||
|
|
390
406
|
proof.run.work_context.id !== proof.run.work_item_id ||
|
|
@@ -399,6 +415,22 @@ export function readWorkflowProof(coordRoot: string, runId: string): WorkflowPro
|
|
|
399
415
|
) {
|
|
400
416
|
throw new Error(`workflow proof at ${path} has an unsupported or mismatched schema`);
|
|
401
417
|
}
|
|
418
|
+
if (
|
|
419
|
+
repoComparisonProblem(proof.repository.before, proof.repository.after) !== undefined ||
|
|
420
|
+
proof.repository.before.dirty_paths_truncated ||
|
|
421
|
+
proof.repository.after.dirty_paths_truncated ||
|
|
422
|
+
!proof.repository.before.branch?.trim() ||
|
|
423
|
+
!proof.repository.after.branch?.trim() ||
|
|
424
|
+
!proof.repository.before.head?.trim() ||
|
|
425
|
+
!proof.repository.after.head?.trim()
|
|
426
|
+
) {
|
|
427
|
+
proof.repository = buildRepoEvidence(proof.repository.before, proof.repository.after);
|
|
428
|
+
proof.unknowns = proof.unknowns.filter((item) => item.code !== "repository_drift_incomplete");
|
|
429
|
+
proof.unknowns.push({
|
|
430
|
+
code: "repository_drift_incomplete",
|
|
431
|
+
message: proof.repository.drift.note!,
|
|
432
|
+
});
|
|
433
|
+
}
|
|
402
434
|
const manifestPath = join(coordRoot, ".harnery", "workflows", runId, "run.json");
|
|
403
435
|
if (proof.execution === undefined && !existsSync(manifestPath)) return proof;
|
|
404
436
|
const manifest = readWorkflowRunManifest(coordRoot, runId);
|
|
@@ -512,13 +544,31 @@ export function renderWorkflowProof(proof: WorkflowProof): string {
|
|
|
512
544
|
: "admission: shadow not needed; no real child dispatch",
|
|
513
545
|
);
|
|
514
546
|
}
|
|
515
|
-
const
|
|
547
|
+
const comparable =
|
|
548
|
+
repoComparisonProblem(proof.repository.before, proof.repository.after) === undefined;
|
|
549
|
+
const repo =
|
|
550
|
+
comparable &&
|
|
551
|
+
!proof.repository.before.dirty_paths_truncated &&
|
|
552
|
+
!proof.repository.after.dirty_paths_truncated &&
|
|
553
|
+
proof.repository.before.branch?.trim() &&
|
|
554
|
+
proof.repository.after.branch?.trim() &&
|
|
555
|
+
proof.repository.before.head?.trim() &&
|
|
556
|
+
proof.repository.after.head?.trim()
|
|
557
|
+
? proof.repository
|
|
558
|
+
: buildRepoEvidence(proof.repository.before, proof.repository.after);
|
|
516
559
|
const drift = repo.drift;
|
|
517
|
-
|
|
518
|
-
`repository:
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
560
|
+
if (!comparable) {
|
|
561
|
+
lines.push(`repository: drift unknown. ${drift.note}`);
|
|
562
|
+
} else {
|
|
563
|
+
const paths =
|
|
564
|
+
drift.dirty_paths_added === null || drift.dirty_paths_cleared === null
|
|
565
|
+
? "dirty-path drift unknown"
|
|
566
|
+
: `${drift.dirty_paths_added.length} dirty added, ${drift.dirty_paths_cleared.length} cleared`;
|
|
567
|
+
lines.push(
|
|
568
|
+
`repository: branch ${repo.before.branch ?? "unknown"} -> ${repo.after.branch ?? "unknown"}; ` +
|
|
569
|
+
`HEAD ${short(repo.before.head)} -> ${short(repo.after.head)}; ${paths}`,
|
|
570
|
+
);
|
|
571
|
+
}
|
|
522
572
|
if (proof.unknowns.length > 0) {
|
|
523
573
|
lines.push(`unknowns: ${proof.unknowns.length}`);
|
|
524
574
|
for (const unknown of proof.unknowns) lines.push(` - ${unknown.message}`);
|
|
@@ -530,26 +580,56 @@ export function renderWorkflowProof(proof: WorkflowProof): string {
|
|
|
530
580
|
function buildRepoEvidence(beforeRaw: RepoSnapshot, afterRaw: RepoSnapshot): WorkflowRepoEvidence {
|
|
531
581
|
const before = normalizeRepoSnapshot(beforeRaw);
|
|
532
582
|
const after = normalizeRepoSnapshot(afterRaw);
|
|
583
|
+
const problem = repoComparisonProblem(before, after);
|
|
584
|
+
if (problem !== undefined) {
|
|
585
|
+
return {
|
|
586
|
+
source: "engine",
|
|
587
|
+
before,
|
|
588
|
+
after,
|
|
589
|
+
drift: {
|
|
590
|
+
branch_changed: null,
|
|
591
|
+
head_changed: null,
|
|
592
|
+
dirty_paths_added: null,
|
|
593
|
+
dirty_paths_cleared: null,
|
|
594
|
+
dirty_paths_retained: null,
|
|
595
|
+
incomplete: true,
|
|
596
|
+
note: `Repository drift could not be verified: ${problem}. Re-check before editing.`,
|
|
597
|
+
},
|
|
598
|
+
};
|
|
599
|
+
}
|
|
600
|
+
const pathsComparable = !before.dirty_paths_truncated && !after.dirty_paths_truncated;
|
|
601
|
+
const branchKnown = Boolean(before.branch && after.branch);
|
|
602
|
+
const headKnown = Boolean(before.head && after.head);
|
|
533
603
|
const beforeDirty = new Set(before.dirty_paths);
|
|
534
604
|
const afterDirty = new Set(after.dirty_paths);
|
|
535
605
|
const retained = after.dirty_paths.filter((path) => beforeDirty.has(path));
|
|
536
|
-
const
|
|
537
|
-
|
|
538
|
-
|
|
606
|
+
const notes = [
|
|
607
|
+
...(!pathsComparable
|
|
608
|
+
? ["Dirty-path drift is unknown because a snapshot list was truncated."]
|
|
609
|
+
: []),
|
|
610
|
+
...(!branchKnown ? ["Branch drift is unknown because a snapshot has no branch."] : []),
|
|
611
|
+
...(!headKnown ? ["HEAD drift is unknown because a snapshot has no HEAD."] : []),
|
|
612
|
+
...(pathsComparable && retained.length > 0
|
|
613
|
+
? ["Snapshots cannot prove whether retained dirty paths changed during the run."]
|
|
614
|
+
: []),
|
|
615
|
+
];
|
|
616
|
+
const incomplete = notes.length > 0;
|
|
539
617
|
return {
|
|
540
618
|
source: "engine",
|
|
541
619
|
before,
|
|
542
620
|
after,
|
|
543
621
|
drift: {
|
|
544
|
-
branch_changed: before.branch !== after.branch,
|
|
545
|
-
head_changed: before.head !== after.head,
|
|
546
|
-
dirty_paths_added:
|
|
547
|
-
|
|
548
|
-
|
|
622
|
+
branch_changed: branchKnown ? before.branch !== after.branch : null,
|
|
623
|
+
head_changed: headKnown ? before.head !== after.head : null,
|
|
624
|
+
dirty_paths_added: pathsComparable
|
|
625
|
+
? after.dirty_paths.filter((path) => !beforeDirty.has(path))
|
|
626
|
+
: null,
|
|
627
|
+
dirty_paths_cleared: pathsComparable
|
|
628
|
+
? before.dirty_paths.filter((path) => !afterDirty.has(path))
|
|
629
|
+
: null,
|
|
630
|
+
dirty_paths_retained: pathsComparable ? retained : null,
|
|
549
631
|
incomplete,
|
|
550
|
-
note: incomplete
|
|
551
|
-
? "Snapshots cannot prove whether retained dirty paths changed during the run, and truncated lists may omit paths."
|
|
552
|
-
: undefined,
|
|
632
|
+
note: incomplete ? notes.join(" ") : undefined,
|
|
553
633
|
},
|
|
554
634
|
};
|
|
555
635
|
}
|
|
@@ -557,14 +637,42 @@ function buildRepoEvidence(beforeRaw: RepoSnapshot, afterRaw: RepoSnapshot): Wor
|
|
|
557
637
|
function normalizeRepoSnapshot(snapshot: RepoSnapshot): WorkflowRepoSnapshot {
|
|
558
638
|
return {
|
|
559
639
|
cwd: snapshot.cwd,
|
|
640
|
+
observation: snapshot.observation ?? "unavailable",
|
|
641
|
+
observation_detail: snapshot.observation
|
|
642
|
+
? clippedOptional(snapshot.observation_detail, 500)
|
|
643
|
+
: "No repository observation was recorded.",
|
|
560
644
|
root: snapshot.root,
|
|
561
|
-
branch: snapshot.branch,
|
|
562
|
-
head: snapshot.head,
|
|
645
|
+
branch: snapshot.branch?.trim() || undefined,
|
|
646
|
+
head: snapshot.head?.trim() || undefined,
|
|
563
647
|
dirty_paths: snapshot.dirty_paths,
|
|
564
648
|
dirty_paths_truncated: snapshot.dirty_paths_truncated,
|
|
565
649
|
};
|
|
566
650
|
}
|
|
567
651
|
|
|
652
|
+
function snapshotFailure(label: string, snapshot: WorkflowRepoSnapshot): string {
|
|
653
|
+
return `${label} snapshot is ${snapshot.observation}${snapshot.observation_detail ? ` (${snapshot.observation_detail})` : ""}`;
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
function repoComparisonProblem(
|
|
657
|
+
before: WorkflowRepoSnapshot,
|
|
658
|
+
after: WorkflowRepoSnapshot,
|
|
659
|
+
): string | undefined {
|
|
660
|
+
if (!observationsComparable(before, after)) {
|
|
661
|
+
return [
|
|
662
|
+
...(before.observation !== "ok" ? [snapshotFailure("before", before)] : []),
|
|
663
|
+
...(after.observation !== "ok" ? [snapshotFailure("after", after)] : []),
|
|
664
|
+
].join("; ");
|
|
665
|
+
}
|
|
666
|
+
const unknownRoots = [
|
|
667
|
+
...(!before.root || !isAbsolute(before.root) ? ["before"] : []),
|
|
668
|
+
...(!after.root || !isAbsolute(after.root) ? ["after"] : []),
|
|
669
|
+
];
|
|
670
|
+
if (unknownRoots.length > 0)
|
|
671
|
+
return `${unknownRoots.join(" and ")} snapshot repository root is unknown`;
|
|
672
|
+
if (before.root !== after.root) return "snapshots name different repository roots";
|
|
673
|
+
return undefined;
|
|
674
|
+
}
|
|
675
|
+
|
|
568
676
|
function buildAdapterCoverage(
|
|
569
677
|
agents: WorkflowAgentProof[],
|
|
570
678
|
claims: Readonly<Record<string, AdapterEvidenceCapability | undefined>> | undefined,
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { existsSync, lstatSync, mkdirSync, readdirSync } from "node:fs";
|
|
3
|
+
import { hostname } from "node:os";
|
|
4
|
+
import { dirname, join } from "node:path";
|
|
5
|
+
import { stateDirMode } from "../storage/modes.ts";
|
|
6
|
+
import { assertWorkflowRunId } from "./approvals.ts";
|
|
7
|
+
import {
|
|
8
|
+
fsyncParentDirectory,
|
|
9
|
+
readJsonRecord,
|
|
10
|
+
stableDigest,
|
|
11
|
+
writeImmutableJson,
|
|
12
|
+
} from "./durable-record.ts";
|
|
13
|
+
|
|
14
|
+
export interface WorkflowRecoveryAuthorization {
|
|
15
|
+
actor: string;
|
|
16
|
+
reason: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface WorkflowRecoveryIntent extends WorkflowRecoveryAuthorization {
|
|
20
|
+
schema_version: 1;
|
|
21
|
+
operation_id: string;
|
|
22
|
+
run_id: string;
|
|
23
|
+
action: "reclaim_unknown_liveness" | "unlock_resume";
|
|
24
|
+
target_sha256: string;
|
|
25
|
+
target: Record<string, unknown>;
|
|
26
|
+
recorded_at: string;
|
|
27
|
+
pid: number;
|
|
28
|
+
host: string;
|
|
29
|
+
outcome: "unknown";
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface WorkflowRecoveryRecord {
|
|
33
|
+
intent: WorkflowRecoveryIntent;
|
|
34
|
+
outcome: "unknown" | "completed";
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function validateWorkflowRecoveryAuthorization(
|
|
38
|
+
authorization: WorkflowRecoveryAuthorization,
|
|
39
|
+
): void {
|
|
40
|
+
for (const [field, limit] of [
|
|
41
|
+
["actor", 200],
|
|
42
|
+
["reason", 2_000],
|
|
43
|
+
] as const) {
|
|
44
|
+
const value = authorization[field];
|
|
45
|
+
if (
|
|
46
|
+
typeof value !== "string" ||
|
|
47
|
+
!value.trim() ||
|
|
48
|
+
value.length > limit ||
|
|
49
|
+
Array.from(value).some(
|
|
50
|
+
(character) => character.charCodeAt(0) < 32 || character.charCodeAt(0) === 127,
|
|
51
|
+
)
|
|
52
|
+
) {
|
|
53
|
+
throw new Error(
|
|
54
|
+
`workflow recovery requires a nonempty ${field} of at most ${limit} characters without control characters`,
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Canonical recovery ledger: immutable intent is durable before effects.
|
|
61
|
+
* Completion cites that exact intent; absence always means unknown. */
|
|
62
|
+
export function beginWorkflowRecovery(
|
|
63
|
+
coordRoot: string,
|
|
64
|
+
input: Omit<
|
|
65
|
+
WorkflowRecoveryIntent,
|
|
66
|
+
"schema_version" | "operation_id" | "recorded_at" | "pid" | "host" | "outcome"
|
|
67
|
+
>,
|
|
68
|
+
): { intent: WorkflowRecoveryIntent; assertCurrent(): void; complete(): void } {
|
|
69
|
+
assertWorkflowRunId(input.run_id);
|
|
70
|
+
validateWorkflowRecoveryAuthorization(input);
|
|
71
|
+
if (!/^[a-f0-9]{64}$/.test(input.target_sha256))
|
|
72
|
+
throw new Error("workflow recovery requires an exact preview digest");
|
|
73
|
+
const intent: WorkflowRecoveryIntent = {
|
|
74
|
+
...input,
|
|
75
|
+
schema_version: 1,
|
|
76
|
+
operation_id: `recovery-${randomUUID()}`,
|
|
77
|
+
recorded_at: new Date().toISOString(),
|
|
78
|
+
pid: process.pid,
|
|
79
|
+
host: hostname(),
|
|
80
|
+
outcome: "unknown",
|
|
81
|
+
};
|
|
82
|
+
const directory = join(
|
|
83
|
+
coordRoot,
|
|
84
|
+
".harnery",
|
|
85
|
+
"workflows",
|
|
86
|
+
input.run_id,
|
|
87
|
+
"recovery",
|
|
88
|
+
intent.operation_id,
|
|
89
|
+
);
|
|
90
|
+
// Sync each newly published directory name too. Syncing only the intent's
|
|
91
|
+
// immediate parent cannot make a newly created ancestor durable.
|
|
92
|
+
const runDirectory = join(coordRoot, ".harnery", "workflows", input.run_id);
|
|
93
|
+
ensureDurableDirectory(runDirectory);
|
|
94
|
+
const ledgerDirectory = join(runDirectory, "recovery");
|
|
95
|
+
ensureDurableDirectory(ledgerDirectory);
|
|
96
|
+
fsyncParentDirectory(ledgerDirectory);
|
|
97
|
+
mkdirSync(directory, { mode: stateDirMode() });
|
|
98
|
+
fsyncParentDirectory(directory);
|
|
99
|
+
const intentPath = join(directory, "intent.json");
|
|
100
|
+
writeImmutableJson(intentPath, intent);
|
|
101
|
+
const assertCurrent = () => {
|
|
102
|
+
if (stableDigest(readJsonRecord(intentPath, "recovery intent")) !== stableDigest(intent))
|
|
103
|
+
throw new Error("workflow recovery audit authority changed");
|
|
104
|
+
};
|
|
105
|
+
return {
|
|
106
|
+
intent,
|
|
107
|
+
assertCurrent,
|
|
108
|
+
complete: () => {
|
|
109
|
+
assertCurrent();
|
|
110
|
+
writeImmutableJson(join(directory, "completion.json"), {
|
|
111
|
+
schema_version: 1,
|
|
112
|
+
operation_id: intent.operation_id,
|
|
113
|
+
intent_sha256: stableDigest(intent),
|
|
114
|
+
outcome: "completed",
|
|
115
|
+
recorded_at: new Date().toISOString(),
|
|
116
|
+
});
|
|
117
|
+
},
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function ensureDurableDirectory(path: string): void {
|
|
122
|
+
if (existsSync(path)) {
|
|
123
|
+
if (!lstatSync(path).isDirectory())
|
|
124
|
+
throw new Error("workflow recovery ledger is not a directory");
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
ensureDurableDirectory(dirname(path));
|
|
128
|
+
try {
|
|
129
|
+
mkdirSync(path, { mode: stateDirMode() });
|
|
130
|
+
} catch (error) {
|
|
131
|
+
if ((error as NodeJS.ErrnoException).code !== "EEXIST" || !lstatSync(path).isDirectory())
|
|
132
|
+
throw error;
|
|
133
|
+
}
|
|
134
|
+
fsyncParentDirectory(path);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export function readWorkflowRecoveryLedger(
|
|
138
|
+
coordRoot: string,
|
|
139
|
+
runId: string,
|
|
140
|
+
): WorkflowRecoveryRecord[] {
|
|
141
|
+
assertWorkflowRunId(runId);
|
|
142
|
+
const directory = join(coordRoot, ".harnery", "workflows", runId, "recovery");
|
|
143
|
+
if (!existsSync(directory)) return [];
|
|
144
|
+
return readdirSync(directory)
|
|
145
|
+
.sort()
|
|
146
|
+
.map((entry) => {
|
|
147
|
+
if (!/^recovery-[a-f0-9-]{36}$/.test(entry))
|
|
148
|
+
throw new Error("invalid workflow recovery ledger entry");
|
|
149
|
+
const intent = readJsonRecord<WorkflowRecoveryIntent>(
|
|
150
|
+
join(directory, entry, "intent.json"),
|
|
151
|
+
"recovery intent",
|
|
152
|
+
);
|
|
153
|
+
if (
|
|
154
|
+
intent.schema_version !== 1 ||
|
|
155
|
+
intent.operation_id !== entry ||
|
|
156
|
+
intent.run_id !== runId ||
|
|
157
|
+
intent.outcome !== "unknown" ||
|
|
158
|
+
!["reclaim_unknown_liveness", "unlock_resume"].includes(intent.action) ||
|
|
159
|
+
!/^[a-f0-9]{64}$/.test(intent.target_sha256)
|
|
160
|
+
) {
|
|
161
|
+
throw new Error("invalid workflow recovery intent authority");
|
|
162
|
+
}
|
|
163
|
+
validateWorkflowRecoveryAuthorization(intent);
|
|
164
|
+
const completionPath = join(directory, entry, "completion.json");
|
|
165
|
+
if (!existsSync(completionPath)) return { intent, outcome: "unknown" };
|
|
166
|
+
const completion = readJsonRecord<{
|
|
167
|
+
schema_version: number;
|
|
168
|
+
operation_id: string;
|
|
169
|
+
intent_sha256: string;
|
|
170
|
+
outcome: string;
|
|
171
|
+
}>(completionPath, "recovery completion");
|
|
172
|
+
if (
|
|
173
|
+
completion.schema_version !== 1 ||
|
|
174
|
+
completion.operation_id !== entry ||
|
|
175
|
+
completion.intent_sha256 !== stableDigest(intent) ||
|
|
176
|
+
completion.outcome !== "completed"
|
|
177
|
+
)
|
|
178
|
+
throw new Error("invalid workflow recovery completion authority");
|
|
179
|
+
return { intent, outcome: "completed" };
|
|
180
|
+
});
|
|
181
|
+
}
|