@cleocode/goal 0.9.4 → 0.9.5
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/CHANGELOG.md +8 -0
- package/dist/cli.js +26 -1
- package/package.json +1 -1
- package/src/cli/status.ts +8 -0
- package/src/engine/harness-identity.ts +95 -0
- package/src/engine/state.ts +9 -1
- package/src/engine/tick.ts +15 -0
- package/src/rpc.ts +3 -0
- package/src/server/app.ts +6 -4
- package/src/verify/artifact.ts +3 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.9.5 — 2026-10-28
|
|
4
|
+
|
|
5
|
+
- **Every run records which harness produced it (T125).** `RunState.harness` is `{name, version, capabilities[]}`: the OpenCode engine records `name: "opencode"`, its serving package version, and the surfaces it can actually drive. The ledger's `start` event carries the same identity, so a repaired run keeps its producer. `goal_status`, the RPC snapshot/view and `goal_provenance` expose it. The field is optional and additive — `schemaVersion` stays 1, and a run written before it existed parses unchanged and is not retro-labelled.
|
|
6
|
+
- **`cleo-goal status --json` reports the harness (T130).** Present when the run recorded one, omitted when it did not; no schema bump. A CLEO that has not yet landed T13875 ignores it.
|
|
7
|
+
- **The Stop-dispatched tick records the harness CLEO declares (T131).** `CLEO_HOOK_PROVIDER=claude-code` records `claude`, `codex` records `codex`, and absent or any other value records `unknown`. Version and capabilities stay `null` on that path because the dispatcher does not supply them, and the Stop payload/transcript is never consulted for identity. The first writer wins: a later tick never renames the harness that produced the run. No `OCGOAL_HARNESS` variable and no `--harness` flag.
|
|
8
|
+
- **`scripts/check-installed-goal.ts` works for a workspace-bound goal (T124).** The check now compares the serving runtime's location with the project checkout (where the plugin actually serves), not the linked worktree the host checks run in. A non-workspace goal is unaffected (`projectRoot === root`), and the check still rejects a runtime serving any other project.
|
|
9
|
+
- **Documentation.** `docs/research/harness-identity.md` records the field, the dispatcher signal, every capability's meaning, the CLEO T13875 contract quote, and the current `cleo doctor goal-runtime` output.
|
|
10
|
+
|
|
3
11
|
## 0.9.4 — 2026-10-28
|
|
4
12
|
|
|
5
13
|
- **Fixed — a contract locked before a validator tightening no longer bricks its run (D10).** A run whose locked generation carries a key the current validator rejects (the reported case: a free-text `budget.rationale` locked under 0.9.2) used to be unresumable: resume called it read-only, `goal_propose_amendment` claimed the disk differed, and `goal_recover_divergence` correctly said nothing differed. The locked bytes are now read leniently **only** to form the prior side of a repair amendment, the repair can be staged and owner-reviewed, and the resume refusal names the failing key and the exact `goal_propose_amendment` call instead of a generic read-only message. Strict validation of new and repaired contracts is unchanged.
|
package/dist/cli.js
CHANGED
|
@@ -6910,7 +6910,7 @@ import { readFileSync as readFileSync4 } from "node:fs";
|
|
|
6910
6910
|
// package.json
|
|
6911
6911
|
var package_default = {
|
|
6912
6912
|
name: "@cleocode/goal",
|
|
6913
|
-
version: "0.9.
|
|
6913
|
+
version: "0.9.5",
|
|
6914
6914
|
description: "CLEO Goal: persistent, host-verified goal mode for OpenCode 2 (and any harness through cleo-goal tick), with a structured goal contract and a live TUI.",
|
|
6915
6915
|
type: "module",
|
|
6916
6916
|
license: "MIT",
|
|
@@ -7706,6 +7706,7 @@ function goalStatus(store, now = Date.now()) {
|
|
|
7706
7706
|
title: run.title,
|
|
7707
7707
|
status: run.status,
|
|
7708
7708
|
...run.priority ? { priority: run.priority } : {},
|
|
7709
|
+
...run.harness ? { harness: run.harness } : {},
|
|
7709
7710
|
turn: run.turn,
|
|
7710
7711
|
reason: run.reason ?? null,
|
|
7711
7712
|
criteria,
|
|
@@ -8251,6 +8252,22 @@ function recordTurn(state, _contract, inputs) {
|
|
|
8251
8252
|
return { turn: state.turn, changed, progress, claimHonoured, ...mined ? { mined } : {}, couldNotSee };
|
|
8252
8253
|
}
|
|
8253
8254
|
|
|
8255
|
+
// src/engine/harness-identity.ts
|
|
8256
|
+
var CLEO_HOOK_PROVIDER = "CLEO_HOOK_PROVIDER";
|
|
8257
|
+
function harnessFromHookProvider(value) {
|
|
8258
|
+
switch ((value ?? "").trim()) {
|
|
8259
|
+
case "claude-code":
|
|
8260
|
+
return "claude";
|
|
8261
|
+
case "codex":
|
|
8262
|
+
return "codex";
|
|
8263
|
+
default:
|
|
8264
|
+
return "unknown";
|
|
8265
|
+
}
|
|
8266
|
+
}
|
|
8267
|
+
function dispatcherIdentity(env = {}) {
|
|
8268
|
+
return { name: harnessFromHookProvider(env[CLEO_HOOK_PROVIDER]), version: null, capabilities: null };
|
|
8269
|
+
}
|
|
8270
|
+
|
|
8254
8271
|
// src/engine/tick.ts
|
|
8255
8272
|
var TICK_LIMITS = { blockerRepeats: 3, stallTurns: 3, recoveryAt: 2, backstopTurns: 200 };
|
|
8256
8273
|
var TICK_COOLDOWN_MS = 1500;
|
|
@@ -8316,6 +8333,14 @@ async function tick(deps) {
|
|
|
8316
8333
|
const record = recordTurn(live, contract, { fingerprint: deps.fingerprint ?? (() => {
|
|
8317
8334
|
return;
|
|
8318
8335
|
}), ...deps.payload ? { payload: deps.payload } : {}, ...deps.readTranscript ? { readTranscript: deps.readTranscript } : {}, now });
|
|
8336
|
+
const identity = dispatcherIdentity(deps.env ?? process.env);
|
|
8337
|
+
if (!live.harness) {
|
|
8338
|
+
live.harness = identity;
|
|
8339
|
+
deps.store.ledger(live.slug, { type: "harness", name: identity.name, version: identity.version, capabilities: identity.capabilities, source: "tick" });
|
|
8340
|
+
log.push(`recorded harness ${identity.name}${identity.name === "unknown" ? " (no CLEO_HOOK_PROVIDER declared)" : ""}`);
|
|
8341
|
+
} else if (live.harness.name !== identity.name && identity.name !== "unknown") {
|
|
8342
|
+
log.push(`harness is already recorded as ${live.harness.name}; the dispatcher declared ${identity.name} - keeping the producer`);
|
|
8343
|
+
}
|
|
8319
8344
|
deps.store.ledger(live.slug, {
|
|
8320
8345
|
type: "turn",
|
|
8321
8346
|
turn: live.turn,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cleocode/goal",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.5",
|
|
4
4
|
"description": "CLEO Goal: persistent, host-verified goal mode for OpenCode 2 (and any harness through cleo-goal tick), with a structured goal contract and a live TUI.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
package/src/cli/status.ts
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
import { parseContract } from "../contract/parse"
|
|
5
5
|
import type { Contract } from "../contract/types"
|
|
6
6
|
import { budgetUse, isTerminal, type RunState, type Status } from "../engine/state"
|
|
7
|
+
import type { HarnessIdentity } from "../engine/harness-identity"
|
|
7
8
|
import type { Store } from "../engine/store"
|
|
8
9
|
|
|
9
10
|
export const STATUS_SCHEMA_VERSION = 1
|
|
@@ -25,6 +26,12 @@ export type GoalStatusJson = {
|
|
|
25
26
|
title: string
|
|
26
27
|
status: Status | "none"
|
|
27
28
|
priority?: "low" | "medium" | "high"
|
|
29
|
+
/**
|
|
30
|
+
* v0.9.5: which harness produced this run and what it can do. Optional and
|
|
31
|
+
* additive: `schemaVersion` stays 1 and a run that predates the field simply
|
|
32
|
+
* omits it, so an older CLEO reads the same shape it always did.
|
|
33
|
+
*/
|
|
34
|
+
harness?: HarnessIdentity
|
|
28
35
|
turn: number
|
|
29
36
|
reason: string | null
|
|
30
37
|
criteria: StatusCriterion[]
|
|
@@ -103,6 +110,7 @@ export function goalStatus(store: Store, now = Date.now()): GoalStatusJson {
|
|
|
103
110
|
title: run.title,
|
|
104
111
|
status: run.status,
|
|
105
112
|
...(run.priority ? { priority: run.priority } : {}),
|
|
113
|
+
...(run.harness ? { harness: run.harness } : {}),
|
|
106
114
|
turn: run.turn,
|
|
107
115
|
reason: run.reason ?? null,
|
|
108
116
|
criteria,
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
// v0.9.5 - which harness produced a run, and what that harness can actually do.
|
|
2
|
+
//
|
|
3
|
+
// Every run records a declared identity: `{name, version, capabilities[]}`. It is
|
|
4
|
+
// additive and optional — `schemaVersion` stays 1 and a run written before this
|
|
5
|
+
// field existed still parses. Two paths declare it, and neither ever guesses:
|
|
6
|
+
//
|
|
7
|
+
// opencode the in-process engine knows its own harness, its serving package
|
|
8
|
+
// version and the surfaces it can actually drive (`OPENCODE_CAPABILITIES`)
|
|
9
|
+
// claude / CLEO's Stop dispatcher names the harness in `CLEO_HOOK_PROVIDER`
|
|
10
|
+
// codex (CLEO T13884). It knows nothing else, so version and capabilities
|
|
11
|
+
// stay null — the run says "not known on this path", never an empty
|
|
12
|
+
// array pretending to be a set
|
|
13
|
+
//
|
|
14
|
+
// Anything else is `unknown`. A transcript (a Stop payload) is data the harness
|
|
15
|
+
// wrote; it never renames the harness. There is deliberately no second
|
|
16
|
+
// convention: no `OCGOAL_HARNESS` variable and no `--harness` flag.
|
|
17
|
+
//
|
|
18
|
+
// The capability names are the seam vocabulary the adapter-seam goal (0.10.0)
|
|
19
|
+
// formalises into a matrix; this goal declares the OpenCode set only. The list
|
|
20
|
+
// is documented in `docs/research/harness-identity.md`.
|
|
21
|
+
|
|
22
|
+
/** The harnesses this version names. `unknown` is a real answer, not a fallback. */
|
|
23
|
+
export type HarnessName = "opencode" | "claude" | "codex" | "unknown"
|
|
24
|
+
|
|
25
|
+
/** The identity a run records. Optional and additive: old runs never had one. */
|
|
26
|
+
export type HarnessIdentity = {
|
|
27
|
+
name: HarnessName
|
|
28
|
+
/** The serving harness's own version, when that path knows it. */
|
|
29
|
+
version: string | null
|
|
30
|
+
/** What the harness can deliver here; null means "not known on this path". */
|
|
31
|
+
capabilities: string[] | null
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* What the OpenCode in-process engine can actually deliver. Each name is a
|
|
36
|
+
* surface this code base really drives:
|
|
37
|
+
*
|
|
38
|
+
* start admit a goal into a session (goal_start, owner-approved)
|
|
39
|
+
* continuation keep the agent working after a turn (session.execution.* → admit)
|
|
40
|
+
* events a live turn/usage event stream (not just a Stop-time notification)
|
|
41
|
+
* tools the agent calls the goal_* tools by name
|
|
42
|
+
* forms a native owner form in the composer (ask tool)
|
|
43
|
+
* verifier an independent verifier child session
|
|
44
|
+
* tui the in-process dashboard/manager UI
|
|
45
|
+
* steering an owner message reaches the run mid-turn
|
|
46
|
+
* version the harness reports its own serving version
|
|
47
|
+
*
|
|
48
|
+
* `transcript` is absent on purpose: OpenCode reports events, so the engine never
|
|
49
|
+
* reads a transcript to learn what happened.
|
|
50
|
+
*/
|
|
51
|
+
export const OPENCODE_CAPABILITIES: readonly string[] = ["start", "continuation", "events", "tools", "forms", "verifier", "tui", "steering", "version"]
|
|
52
|
+
|
|
53
|
+
/** The identity the OpenCode engine records at goal start. */
|
|
54
|
+
export function opencodeIdentity(version: string | null | undefined): HarnessIdentity {
|
|
55
|
+
return { name: "opencode", version: version ?? null, capabilities: [...OPENCODE_CAPABILITIES] }
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** CLEO's dispatcher sets this on the `cleo-goal tick` child (CLEO T13884). */
|
|
59
|
+
export const CLEO_HOOK_PROVIDER = "CLEO_HOOK_PROVIDER"
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Map the dispatcher's provider id onto a harness name. Only the two values CLEO
|
|
63
|
+
* actually sets are mapped; everything else — including absent — is `unknown`.
|
|
64
|
+
* Nothing is inferred from the Stop payload or the transcript.
|
|
65
|
+
*/
|
|
66
|
+
export function harnessFromHookProvider(value: string | undefined): HarnessName {
|
|
67
|
+
switch ((value ?? "").trim()) {
|
|
68
|
+
case "claude-code":
|
|
69
|
+
return "claude"
|
|
70
|
+
case "codex":
|
|
71
|
+
return "codex"
|
|
72
|
+
default:
|
|
73
|
+
return "unknown"
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* The identity a Stop-dispatched tick records: the declared name only. CLEO does
|
|
79
|
+
* not tell the dispatcher the harness version or its capabilities, so they stay
|
|
80
|
+
* null rather than being invented from what the payload happens to contain.
|
|
81
|
+
*/
|
|
82
|
+
export function dispatcherIdentity(env: Record<string, string | undefined> = {}): HarnessIdentity {
|
|
83
|
+
return { name: harnessFromHookProvider(env[CLEO_HOOK_PROVIDER]), version: null, capabilities: null }
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** A recorded identity is usable only if it has the documented shape. */
|
|
87
|
+
export function isHarnessIdentity(value: unknown): value is HarnessIdentity {
|
|
88
|
+
if (!value || typeof value !== "object") return false
|
|
89
|
+
const identity = value as Partial<HarnessIdentity>
|
|
90
|
+
return (
|
|
91
|
+
(identity.name === "opencode" || identity.name === "claude" || identity.name === "codex" || identity.name === "unknown") &&
|
|
92
|
+
(identity.version === null || typeof identity.version === "string") &&
|
|
93
|
+
(identity.capabilities === null || (Array.isArray(identity.capabilities) && identity.capabilities.every((c) => typeof c === "string")))
|
|
94
|
+
)
|
|
95
|
+
}
|
package/src/engine/state.ts
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
// model can report progress, claim, block, flag and wait; only the owner (or the
|
|
3
3
|
// host acting on owner policy) can start, resume, pause, abort or approve.
|
|
4
4
|
import type { Contract } from "../contract/types"
|
|
5
|
+
import type { HarnessIdentity } from "./harness-identity"
|
|
5
6
|
|
|
6
7
|
export type Status =
|
|
7
8
|
| "running"
|
|
@@ -39,6 +40,12 @@ export type RunState = {
|
|
|
39
40
|
title: string
|
|
40
41
|
/** T061: copied from the contract at start so the registry/rebuild keep it. */
|
|
41
42
|
priority?: "low" | "medium" | "high"
|
|
43
|
+
/**
|
|
44
|
+
* v0.9.5: which harness produced this run and what it can deliver. Optional
|
|
45
|
+
* and additive — a run written before this field existed has none and still
|
|
46
|
+
* parses as `version: 1`.
|
|
47
|
+
*/
|
|
48
|
+
harness?: HarnessIdentity
|
|
42
49
|
sessionID: string
|
|
43
50
|
runId: string
|
|
44
51
|
lock: string
|
|
@@ -83,13 +90,14 @@ export type RunState = {
|
|
|
83
90
|
}
|
|
84
91
|
}
|
|
85
92
|
|
|
86
|
-
export function initialRun(contract: Contract, args: { sessionID: string; runId: string; lock: string; commit?: string; now?: number }): RunState {
|
|
93
|
+
export function initialRun(contract: Contract, args: { sessionID: string; runId: string; lock: string; commit?: string; harness?: HarnessIdentity; now?: number }): RunState {
|
|
87
94
|
const now = args.now ?? Date.now()
|
|
88
95
|
return {
|
|
89
96
|
version: 1,
|
|
90
97
|
slug: contract.id,
|
|
91
98
|
title: contract.title,
|
|
92
99
|
...(contract.priority ? { priority: contract.priority } : {}),
|
|
100
|
+
...(args.harness ? { harness: args.harness } : {}),
|
|
93
101
|
sessionID: args.sessionID,
|
|
94
102
|
runId: args.runId,
|
|
95
103
|
lock: args.lock,
|
package/src/engine/tick.ts
CHANGED
|
@@ -22,6 +22,7 @@ import { renderTailNote } from "../contract/render"
|
|
|
22
22
|
import { budgetUse, setStatus, TERMINAL as TERMINAL_STATUSES, type RunState } from "./state"
|
|
23
23
|
import { continuationTail, decide, type LoopLimits, type LoopSignals } from "./loop"
|
|
24
24
|
import { recordTurn } from "./tick-record"
|
|
25
|
+
import { dispatcherIdentity } from "./harness-identity"
|
|
25
26
|
import type { VerifyOutcome } from "../verify/pipeline"
|
|
26
27
|
|
|
27
28
|
/** The plugin's own defaults, so a tick agrees with the adapter unless overridden. */
|
|
@@ -49,6 +50,8 @@ export type TickDeps = {
|
|
|
49
50
|
verify?: (state: RunState, contract: Contract, contractText: string) => Promise<VerifyOutcome>
|
|
50
51
|
/** The raw harness Stop payload, when the harness passed one. */
|
|
51
52
|
payload?: string
|
|
53
|
+
/** The dispatcher's environment. Defaults to the real one. */
|
|
54
|
+
env?: Record<string, string | undefined>
|
|
52
55
|
readTranscript?: (path: string) => string | undefined
|
|
53
56
|
limits?: LoopLimits
|
|
54
57
|
cooldownMs?: number
|
|
@@ -130,6 +133,18 @@ export async function tick(deps: TickDeps): Promise<TickResult> {
|
|
|
130
133
|
|
|
131
134
|
// ── 1. RECORD the turn, through OCG's own store, in the adapter's own shape.
|
|
132
135
|
const record = recordTurn(live, contract, { fingerprint: deps.fingerprint ?? (() => undefined), ...(deps.payload ? { payload: deps.payload } : {}), ...(deps.readTranscript ? { readTranscript: deps.readTranscript } : {}), now })
|
|
136
|
+
// ── 1a. DECLARE the harness that produced this run, once. CLEO's dispatcher
|
|
137
|
+
// names it (`CLEO_HOOK_PROVIDER`); absent or unrecognised is `unknown`, never a
|
|
138
|
+
// guess from the payload. A run already carries its producer, so it is never
|
|
139
|
+
// renamed by a later tick.
|
|
140
|
+
const identity = dispatcherIdentity(deps.env ?? process.env)
|
|
141
|
+
if (!live.harness) {
|
|
142
|
+
live.harness = identity
|
|
143
|
+
deps.store.ledger(live.slug, { type: "harness", name: identity.name, version: identity.version, capabilities: identity.capabilities, source: "tick" })
|
|
144
|
+
log.push(`recorded harness ${identity.name}${identity.name === "unknown" ? " (no CLEO_HOOK_PROVIDER declared)" : ""}`)
|
|
145
|
+
} else if (live.harness.name !== identity.name && identity.name !== "unknown") {
|
|
146
|
+
log.push(`harness is already recorded as ${live.harness.name}; the dispatcher declared ${identity.name} - keeping the producer`)
|
|
147
|
+
}
|
|
133
148
|
deps.store.ledger(live.slug, {
|
|
134
149
|
type: "turn",
|
|
135
150
|
turn: live.turn,
|
package/src/rpc.ts
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
// function), so neither side needs @opencode/plugin at runtime.
|
|
3
3
|
const anyObject = { type: "object", additionalProperties: true } as const
|
|
4
4
|
import type { Handoff } from "./engine/state"
|
|
5
|
+
import type { HarnessIdentity } from "./engine/harness-identity"
|
|
5
6
|
import type { LaunchView } from "./engine/launches"
|
|
6
7
|
export type { LaunchView } from "./engine/launches"
|
|
7
8
|
// V2 RPC's schema adapter rejects regex patterns. Length is declared here;
|
|
@@ -120,6 +121,8 @@ export type GoalView = {
|
|
|
120
121
|
/** T106: the tuple goal_manage accepts, so a caller never loses a round to
|
|
121
122
|
* discovery after a generation change — key, runId, sessionID and lock. */
|
|
122
123
|
target?: GoalTarget
|
|
124
|
+
/** v0.9.5: the harness that produced this run (absent on older runs). */
|
|
125
|
+
harness?: HarnessIdentity
|
|
123
126
|
turn: number
|
|
124
127
|
timeline: TimelineEntry[]
|
|
125
128
|
activeMs: number
|
package/src/server/app.ts
CHANGED
|
@@ -12,6 +12,7 @@ import type { Contract } from "../contract/types"
|
|
|
12
12
|
import { isHostCheck } from "../contract/types"
|
|
13
13
|
import { GoalRpc, type GoalSummary, type GoalTarget, type GoalView, type ManageAction } from "../rpc"
|
|
14
14
|
import { account, budgetUse, initialRun, isActive, isTerminal, modelCan, ownerCan, setStatus, type Handoff, type PendingKind, type RunState, type Status } from "../engine/state"
|
|
15
|
+
import { isHarnessIdentity, opencodeIdentity } from "../engine/harness-identity"
|
|
15
16
|
import { Store } from "../engine/store"
|
|
16
17
|
import { Registry, byPriorityThenRecency } from "../engine/registry"
|
|
17
18
|
import { LaunchStore, launchChoices, launchView, processAlive, type LaunchActivity, type LaunchRequest } from "../engine/launches"
|
|
@@ -329,7 +330,7 @@ export class GoalApp {
|
|
|
329
330
|
|
|
330
331
|
const kept = this.store.keepCorruptRun(slug, text)
|
|
331
332
|
const lastTurn = this.store.recent(slug, 5000).filter((e) => e.type === "turn" && typeof e.turn === "number").at(-1)
|
|
332
|
-
const state = initialRun(parsed.contract, { sessionID, runId, lock, ...(typeof start.commit === "string" ? { commit: start.commit } : {}) })
|
|
333
|
+
const state = initialRun(parsed.contract, { sessionID, runId, lock, ...(typeof start.commit === "string" ? { commit: start.commit } : {}), ...(isHarnessIdentity(start.harness) ? { harness: start.harness } : {}) })
|
|
333
334
|
state.turn = Number(lastTurn?.turn ?? 0)
|
|
334
335
|
for (const id of Object.keys(state.criteria)) {
|
|
335
336
|
const recorded = this.store.recent(slug, 5000).findLast((e) => e.type === "criterion" && e.criterion === id) as any
|
|
@@ -560,6 +561,7 @@ export class GoalApp {
|
|
|
560
561
|
runId: state.runId,
|
|
561
562
|
sessionID: state.sessionID,
|
|
562
563
|
lock: state.lock,
|
|
564
|
+
...(state.harness ? { harness: state.harness } : {}),
|
|
563
565
|
target: this.targetOf(state),
|
|
564
566
|
turn: state.turn,
|
|
565
567
|
timeline: this.timelineOf(state),
|
|
@@ -1838,7 +1840,7 @@ export class GoalApp {
|
|
|
1838
1840
|
const current = this.load(slug, true)
|
|
1839
1841
|
if ("error" in current || current.lock !== options.expectedLock) return "Refused: contract changed during launch rehearsal; no run created. Prepare a new review."
|
|
1840
1842
|
}
|
|
1841
|
-
const state = initialRun(contract, { sessionID, runId: newRunId(), lock, commit: headCommit(exec) })
|
|
1843
|
+
const state = initialRun(contract, { sessionID, runId: newRunId(), lock, commit: headCommit(exec), harness: opencodeIdentity(this.packageVersion) })
|
|
1842
1844
|
if (options.handoff) state.handoff = options.handoff
|
|
1843
1845
|
state.fingerprint = fingerprint(exec)
|
|
1844
1846
|
if (rehearsal.length) {
|
|
@@ -1851,7 +1853,7 @@ export class GoalApp {
|
|
|
1851
1853
|
this.store.evidence(slug, state.runId, "contract", { lock, text })
|
|
1852
1854
|
await Bun.write(join(this.store.dir(slug), "evidence", state.runId, "contract.yaml"), text)
|
|
1853
1855
|
this.runs.set(sessionID, state)
|
|
1854
|
-
this.store.ledger(slug, { type: "start", runId: state.runId, sessionID, source, lock, commit: state.base.commit, ...(options.decisionId ? { decisionId: options.decisionId } : {}) })
|
|
1856
|
+
this.store.ledger(slug, { type: "start", runId: state.runId, sessionID, source, lock, commit: state.base.commit, ...(state.harness ? { harness: state.harness } : {}), ...(options.decisionId ? { decisionId: options.decisionId } : {}) })
|
|
1855
1857
|
// T027 / v0.5.0: record the CLEO linkage and health (CLI-only). A project
|
|
1856
1858
|
// with no CLEO CLI records nothing and behaves EXACTLY as before; a project
|
|
1857
1859
|
// with the CLI gets an honest state, and nothing is ever installed here.
|
|
@@ -3224,7 +3226,7 @@ export class GoalApp {
|
|
|
3224
3226
|
artifacts: this.artifacts,
|
|
3225
3227
|
status: entry?.state?.status ?? "unknown",
|
|
3226
3228
|
features: { server: entry?.features?.server === true, tui: entry?.features?.tui === true, rpc: entry?.features?.rpc === true },
|
|
3227
|
-
...(state ? { sessionID: state.sessionID, runId: state.runId, lock: state.lock } : {}),
|
|
3229
|
+
...(state ? { sessionID: state.sessionID, runId: state.runId, lock: state.lock, ...(state.harness ? { harness: state.harness } : {}) } : {}),
|
|
3228
3230
|
}
|
|
3229
3231
|
// T115: this is a READ-ONLY probe. It returns the runtime identity and
|
|
3230
3232
|
// writes NOTHING under .opencode/goals, so no read can look like an agent
|
package/src/verify/artifact.ts
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
import { createHash } from "node:crypto"
|
|
6
6
|
import { lstatSync, readFileSync, readdirSync } from "node:fs"
|
|
7
7
|
import { join } from "node:path"
|
|
8
|
+
import type { HarnessIdentity } from "../engine/harness-identity"
|
|
8
9
|
|
|
9
10
|
export type ArtifactHashes = Record<string, string>
|
|
10
11
|
export type ArtifactSource = { type: string; target?: string; version?: string; outdated?: boolean; path?: string }
|
|
@@ -24,6 +25,8 @@ export type RuntimeProvenance = {
|
|
|
24
25
|
sessionID?: string
|
|
25
26
|
runId?: string
|
|
26
27
|
lock?: string
|
|
28
|
+
/** v0.9.5: the harness serving this run, as the run recorded it. */
|
|
29
|
+
harness?: HarnessIdentity
|
|
27
30
|
}
|
|
28
31
|
|
|
29
32
|
const entrypoints = ["package.json", "server.ts", "tui.tsx", "rpc.ts"]
|