weir-harness 1.2.0 → 1.2.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/cordis.patch.yml +0 -2
- package/lib/client.blackboard-model.js +172 -56
- package/lib/client.capability-manager.js +0 -1
- package/lib/client.capability-model.js +378 -31
- package/lib/client.chain-model.js +80 -0
- package/lib/client.disabled-categories-editor.js +22 -0
- package/lib/client.edit-lock-maintenance.js +239 -246
- package/lib/client.edit-lock-panel.js +110 -0
- package/lib/client.hash-edit-model.js +48 -0
- package/lib/client.js +267 -1549
- package/lib/client.lsp-model.js +92 -0
- package/lib/client.lsp-panel.js +11 -16
- package/lib/client.lsp-toggle.js +18 -0
- package/lib/client.notify-permissions.js +112 -20
- package/lib/client.notify-web.js +14 -11
- package/lib/client.robash-model.js +26 -0
- package/lib/client.settings-page.js +287 -17
- package/lib/client.worktree-model.js +225 -0
- package/package.json +5 -2
- package/src/blackboard/durable.js +12 -0
- package/src/blackboard/index.js +2 -2
- package/src/blackboard/kernel.js +23 -6
- package/src/blackboard/notify.js +10 -3
- package/src/blackboard/remote.js +31 -13
- package/src/blackboard/storage.js +13 -5
- package/src/capabilities/apply-engine.js +189 -101
- package/src/capabilities/defaults-transaction.js +136 -1
- package/src/capabilities/lifecycle-snapshot.js +83 -204
- package/src/capabilities/mcp-admission.js +1 -23
- package/src/capabilities/mcp-drain.js +30 -19
- package/src/capabilities/mcp-facade.js +23 -3
- package/src/capabilities/preset-library.js +247 -0
- package/src/capabilities/preset-package.js +212 -1
- package/src/capabilities/same-name-policy.js +3 -41
- package/src/capabilities/selection-notify.js +324 -21
- package/src/capabilities/skill-office-adapter.js +7 -1
- package/src/capabilities/skill-selection-plugin.js +315 -729
- package/src/capabilities/store/checked-contract.d.ts +5 -0
- package/src/capabilities/store/fs-adapter.js +52 -0
- package/src/capabilities/store/lock.js +86 -0
- package/src/capabilities/store/paths.js +5 -0
- package/src/capabilities/store/store.js +105 -5
- package/src/creative-guide/guide.js +1 -1
- package/src/delegate/child-composition.js +258 -0
- package/src/delegate/group-coordinator.js +25 -27
- package/src/delegate/index.js +21 -2
- package/src/delegate/rehydrate.js +24 -4
- package/src/delegate/robash-guard-core.js +2 -2
- package/src/delegate/spawn-adapter.js +214 -100
- package/src/delegate/status-tool.js +9 -6
- package/src/delegate/supervision-mount.js +35 -6
- package/src/delegate/tool.js +29 -4
- package/src/{shared → delegate}/whitelist-defaults.js +2 -2
- package/src/edit-lock/admin-ledger.js +8 -11
- package/src/edit-lock/admin-recovery.js +2 -1
- package/src/edit-lock/admission.js +12 -2
- package/src/edit-lock/binding-guidance.js +28 -10
- package/src/edit-lock/binding-machine.js +312 -0
- package/src/edit-lock/derived-facts.js +67 -0
- package/src/edit-lock/index.js +44 -244
- package/src/edit-lock/inspect.js +113 -1
- package/src/edit-lock/lifecycle.js +8 -5
- package/src/edit-lock/limits.js +61 -0
- package/src/edit-lock/maintenance.js +5 -2
- package/src/edit-lock/manager.js +6 -2
- package/src/edit-lock/operation-history.js +1 -1
- package/src/edit-lock/peer-client.js +9 -2
- package/src/edit-lock/peer-transport.js +7 -1
- package/src/edit-lock/remote.js +11 -4
- package/src/edit-lock/state.js +2 -4
- package/src/edit-lock/view.js +10 -11
- package/src/lsp/admin.js +2 -24
- package/src/lsp/index.js +1 -1
- package/src/lsp/manager.js +1 -1
- package/src/notify/permissions-admin.js +1 -1
- package/src/{lsp/child-process.js → process/index.js} +23 -6
- package/src/settings/env-admin.js +1 -1
- package/src/settings/index.js +1 -1
- package/src/settings/sections.js +0 -54
- package/src/shared/audit.js +12 -0
- package/src/shared/child-scope.js +3 -1
- package/src/shared/http-json.js +28 -0
- package/src/{delegate/audit-readers.js → shared/terminal-evidence.js} +34 -4
- package/src/worktree/errors.js +3 -0
- package/src/worktree/index.js +1 -2
- package/src/worktree/lane-core.js +641 -0
- package/src/worktree/lane-views.js +147 -0
- package/src/worktree/lane-workflows.js +853 -0
- package/src/worktree/lanes.js +24 -1513
- package/src/worktree/runner.js +5 -4
- package/src/capabilities/content-pin.js +0 -88
- package/src/capabilities/content-refresh.js +0 -331
- package/src/capabilities/distribution-manifest.js +0 -78
- package/src/capabilities/isolated-exec.js +0 -106
- package/src/capabilities/publisher.js +0 -129
- package/src/capabilities/skill-admission.js +0 -181
- package/src/capabilities/update-lifecycle.js +0 -106
- package/src/edit-lock/remote-service.js +0 -28
- package/src/worktree/contract-guidance.test.js +0 -100
package/src/worktree/runner.js
CHANGED
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
// Process runners for the worktree service.
|
|
2
2
|
//
|
|
3
3
|
// - gitRunner: argv-only execution of git through ctx.subprocess (no shell,
|
|
4
|
-
// no interpolation). The executable resolves through the
|
|
5
|
-
// extended resolver because a LaunchServices-started desktop
|
|
6
|
-
// minimal PATH (S21). Output is collected per stream with a
|
|
4
|
+
// no interpolation). The executable resolves through the process
|
|
5
|
+
// subsystem's extended resolver because a LaunchServices-started desktop
|
|
6
|
+
// host has a minimal PATH (S21). Output is collected per stream with a
|
|
7
|
+
// deadline.
|
|
7
8
|
// - shellRunner: user-configured setup / verification commands (shell
|
|
8
9
|
// strings) through ctx.shell with the calling session's per-call sandbox
|
|
9
10
|
// policy — the same boundary the bash tool enforces (S23 / S26 S-G).
|
|
10
11
|
import { existsSync, readdirSync, statSync } from 'node:fs'
|
|
11
12
|
import { homedir } from 'node:os'
|
|
12
|
-
import { childEnvironment, extraBinDirectories, resolveExecutable } from '../
|
|
13
|
+
import { childEnvironment, extraBinDirectories, resolveExecutable } from '../process/index.js'
|
|
13
14
|
import { resolveDerivedSetup } from './pkgmgr.js'
|
|
14
15
|
|
|
15
16
|
/** Vendor git install locations, probed when PATH resolution fails. */
|
|
@@ -1,88 +0,0 @@
|
|
|
1
|
-
// Task 11.6 of the session-capability-manager change: session content pins,
|
|
2
|
-
// explicit refresh, local-change confirmation and GC.
|
|
3
|
-
//
|
|
4
|
-
// The first time a session accepts Skill content, the pin durably records
|
|
5
|
-
// the body, relatively-referenced assets, resourceBase and manifest through
|
|
6
|
-
// the group-2 store. Non-managed sources are copied first and verified as a
|
|
7
|
-
// consistent snapshot — a source that changes mid-copy is retried or
|
|
8
|
-
// refused. After an update is published the session keeps using its pinned
|
|
9
|
-
// content and reports update-available; adopting new content needs an
|
|
10
|
-
// explicit refresh with a content-revision CAS (the 4.5 refresh engine),
|
|
11
|
-
// which never changes the selection identity and never removes history.
|
|
12
|
-
// With the original install directory gone, a fully pinned identity stays
|
|
13
|
-
// usable. When local content diverges from the last managed digest,
|
|
14
|
-
// publication pauses and the user chooses overwrite (report: local changes
|
|
15
|
-
// discarded) or leave unchanged (report: skipped). A generation whose
|
|
16
|
-
// reference count is non-zero (live/durable sessions, subagents/forks or
|
|
17
|
-
// unsettled receipts) is never collected; anomalous recovery keeps
|
|
18
|
-
// conservatively.
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* The pin snapshot recorded for one accepted identity (11.6).
|
|
22
|
-
* @param {{ identity: unknown, body: string, assets: Record<string, string>, resourceBase: unknown, manifest: unknown }} input
|
|
23
|
-
*/
|
|
24
|
-
export function contentPinOf({ identity, body, assets = {}, resourceBase = null, manifest = null }) {
|
|
25
|
-
if (typeof body !== 'string') throw new TypeError('a content pin needs a body string')
|
|
26
|
-
return {
|
|
27
|
-
identity,
|
|
28
|
-
body,
|
|
29
|
-
assets: { ...assets },
|
|
30
|
-
resourceBase,
|
|
31
|
-
manifest,
|
|
32
|
-
revision: 1,
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
/**
|
|
37
|
-
* Consistent-snapshot verification for non-managed sources (11.6): two reads
|
|
38
|
-
* of the same logical content must match exactly, or the copy is refused.
|
|
39
|
-
* @param {string} first @param {string} second
|
|
40
|
-
*/
|
|
41
|
-
export function snapshotVerdict(first, second) {
|
|
42
|
-
if (first !== second) return { ok: false, reason: 'source content changed mid-copy — the pin is refused (retry only if the source settles)' }
|
|
43
|
-
return { ok: true }
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
/**
|
|
47
|
-
* Whether a pinned identity stays usable with its origin directory gone
|
|
48
|
-
* (11.6): a full pin (body + manifest) is sufficient on its own.
|
|
49
|
-
* @param {{ body?: unknown, manifest?: unknown }} pin
|
|
50
|
-
*/
|
|
51
|
-
export function pinSelfSufficient(pin) {
|
|
52
|
-
return typeof pin?.body === 'string' && pin.body.length > 0 && pin.manifest !== null && pin.manifest !== undefined
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
/**
|
|
56
|
-
* The refresh verdict for adopting published content (11.6): an explicit,
|
|
57
|
-
* CAS-guarded refresh that never changes the selection identity.
|
|
58
|
-
* @param {{ expectedRevision: number, currentRevision: number }} input
|
|
59
|
-
*/
|
|
60
|
-
export function refreshVerdict({ expectedRevision, currentRevision }) {
|
|
61
|
-
if (expectedRevision !== currentRevision) {
|
|
62
|
-
return { refresh: false, reason: `content revision moved: expected ${expectedRevision}, found ${currentRevision} — nothing adopted (retry against the fresh revision)` }
|
|
63
|
-
}
|
|
64
|
-
return { refresh: true }
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
/**
|
|
68
|
-
* The local-change decision (11.6): publication pauses on divergence and the
|
|
69
|
-
* user's choice is reported, never silent.
|
|
70
|
-
* @param {'overwrite'|'leave-unchanged'} decision
|
|
71
|
-
*/
|
|
72
|
-
export function localChangeOutcome(decision) {
|
|
73
|
-
if (decision === 'overwrite') return { publish: true, report: 'local modifications were discarded by overwrite' }
|
|
74
|
-
if (decision === 'leave-unchanged') return { publish: false, report: 'publication skipped; local content left unchanged' }
|
|
75
|
-
return { publish: false, report: 'publication paused — no decision yet (overwrite / leave unchanged required)' }
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
/**
|
|
79
|
-
* The GC reference-count gate (11.6): any live reference blocks collection;
|
|
80
|
-
* an unreadable or anomalous record keeps conservatively.
|
|
81
|
-
* @param {{ liveSessions?: number, durablePins?: number, subagentRefs?: number, unsettledReceipts?: number, anomaly?: boolean }} counts
|
|
82
|
-
*/
|
|
83
|
-
export function gcVerdict(counts) {
|
|
84
|
-
if (counts.anomaly) return { collect: false, reason: 'anomalous recovery state — kept conservatively' }
|
|
85
|
-
const held = (counts.liveSessions ?? 0) + (counts.durablePins ?? 0) + (counts.subagentRefs ?? 0) + (counts.unsettledReceipts ?? 0)
|
|
86
|
-
if (held > 0) return { collect: false, reason: `reference count non-zero (${held}) — generation kept` }
|
|
87
|
-
return { collect: true }
|
|
88
|
-
}
|
|
@@ -1,331 +0,0 @@
|
|
|
1
|
-
// Content refresh engine of the session capability manager (design D2,
|
|
2
|
-
// tasks 4.5). A refresh re-scans the content of the CURRENTLY selected skills
|
|
3
|
-
// and durably records fresh content handles in the session's CONTENT unit —
|
|
4
|
-
// an independent store record with its own revision and receipts, never the
|
|
5
|
-
// selection record. One refresh is a four-step transaction:
|
|
6
|
-
// 1. Locate the session server-side from the AUTHENTICATED session; the
|
|
7
|
-
// client never supplies cwd, scope roots or server config.
|
|
8
|
-
// 2. Validate requestId, the expected content revision, the expected
|
|
9
|
-
// selection revision and the optional exact identity subset.
|
|
10
|
-
// 3. Read the CURRENT selection, verify every requested identity is still
|
|
11
|
-
// selected (a removed skill is never re-published), collect and verify
|
|
12
|
-
// fresh content handles server-side, then RE-READ the selection
|
|
13
|
-
// immediately before committing: if its revision moved, the refresh is
|
|
14
|
-
// rejected — a skill removed while a refresh was collecting is never
|
|
15
|
-
// published by that refresh.
|
|
16
|
-
// 4. CAS-commit the content handles through the group-2 store with an
|
|
17
|
-
// independent requestId receipt.
|
|
18
|
-
// The whole transaction runs through the SAME commit coordinator as selection
|
|
19
|
-
// Applies (the apply engine's per-session serialization point, exposed as
|
|
20
|
-
// `coordinate`), so a refresh and an Apply never interleave in-process;
|
|
21
|
-
// cross-process exclusion is the store lock plus the pre-commit selection
|
|
22
|
-
// re-check. A refresh enters no admission fence and publishes nothing
|
|
23
|
-
// in-memory: the recorded `selectionRevision` marks exactly which selection
|
|
24
|
-
// authority the handles were verified against, so any consumer can detect
|
|
25
|
-
// staleness after a later Apply.
|
|
26
|
-
// Audit is scheduled best-effort after the response and reuses the registered
|
|
27
|
-
// capability-apply type (adding a type would touch src/shared/audit.js, which
|
|
28
|
-
// is outside this group); its failure never rolls back accepted policy.
|
|
29
|
-
import { digestOf } from './store/record.js'
|
|
30
|
-
import { isSegment } from './store/paths.js'
|
|
31
|
-
import { createSkillIdentity, skillIdentityKey } from './skill-identity.js'
|
|
32
|
-
import { AUDIT_TYPES } from '../shared/audit.js'
|
|
33
|
-
|
|
34
|
-
/** Refresh acceptance is audited under the registered capability-apply type
|
|
35
|
-
* with an `operation: 'refresh'` marker; the emit site never invents a type
|
|
36
|
-
* literal and never touches session.append (cold-read red line). */
|
|
37
|
-
export const REFRESH_AUDIT_TYPE = AUDIT_TYPES.capabilityApply
|
|
38
|
-
|
|
39
|
-
/**
|
|
40
|
-
* @typedef {{ status: 'refreshed', revision: number, receipt: unknown, selectionRevision: number, contents: unknown[] }
|
|
41
|
-
* | { status: 'duplicate', revision: number, receipt: unknown, selectionRevision?: unknown, warnings?: string[] }
|
|
42
|
-
* | { status: 'rejected', reason: string, step: number, missing?: string[], current?: unknown, receipt?: unknown }
|
|
43
|
-
* | { status: 'write-failed', step: 4 }
|
|
44
|
-
* | { status: 'indeterminate', step: 4 }} RefreshResult
|
|
45
|
-
* @typedef {{ status: 'found', revision: number, receipt: unknown, selectionRevision: unknown, contents: unknown[] }
|
|
46
|
-
* | { status: 'not-found', revision: number|null }
|
|
47
|
-
* | { status: 'rejected', reason: string }} RefreshReceiptQueryResult
|
|
48
|
-
*/
|
|
49
|
-
|
|
50
|
-
const message = error => error instanceof Error ? error.message : String(error)
|
|
51
|
-
|
|
52
|
-
/**
|
|
53
|
-
* Request digest: the full normalized request payload, so any field change
|
|
54
|
-
* under a reused requestId is a different request. `skills` is the sorted
|
|
55
|
-
* identity-key vector (null when the request refreshes the whole selection).
|
|
56
|
-
* @param {{ expectedRevision: number, expectedSelectionRevision: number, skills: string[]|null }} normalized
|
|
57
|
-
*/
|
|
58
|
-
export function digestRefreshRequest(normalized) {
|
|
59
|
-
return digestOf({
|
|
60
|
-
expectedRevision: normalized.expectedRevision,
|
|
61
|
-
expectedSelectionRevision: normalized.expectedSelectionRevision,
|
|
62
|
-
skills: normalized.skills,
|
|
63
|
-
})
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
/** @param {unknown} payload @returns {unknown[]} */
|
|
67
|
-
function safeSkills(payload) {
|
|
68
|
-
const value = /** @type {Record<string, unknown>} */ (payload ?? {})
|
|
69
|
-
return Array.isArray(value.skills) ? value.skills : []
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
/**
|
|
73
|
-
* @param {{
|
|
74
|
-
* store: { read(unit: unknown): Promise<any>, commit(unit: unknown, expectedRevision: number, mutate: (payload: unknown, info: { revision: number }) => unknown, request?: { requestId?: string, requestDigest?: string }): Promise<any> },
|
|
75
|
-
* locateSession: (authSession: unknown) => Promise<{ sessionId: string, cwd?: string } | null> | { sessionId: string, cwd?: string } | null,
|
|
76
|
-
* coordinate: <T>(sessionId: string, task: () => Promise<T>) => Promise<T>,
|
|
77
|
-
* collect: (options: { cwd?: string, scope: { session: { id: string } } }) => Promise<{ complete: boolean, candidates: any[] }>,
|
|
78
|
-
* verifyContent?: (candidate: unknown) => Promise<void> | void,
|
|
79
|
-
* audit?: (session: { id?: string }, type: string, data?: unknown) => void,
|
|
80
|
-
* warn?: (message: string) => void,
|
|
81
|
-
* trace?: (event: string, data?: unknown) => void,
|
|
82
|
-
* }} options
|
|
83
|
-
*/
|
|
84
|
-
export function createContentRefresh(options) {
|
|
85
|
-
const { store, locateSession, coordinate, collect, verifyContent, audit, warn = () => {}, trace = () => {} } = options
|
|
86
|
-
if (typeof coordinate !== 'function') throw new TypeError('The shared commit coordinator is required')
|
|
87
|
-
if (typeof collect !== 'function') throw new TypeError('A server-side content collector is required')
|
|
88
|
-
|
|
89
|
-
/** Best-effort audit scheduled after the response; failures warn and are swallowed, never rolling back policy. */
|
|
90
|
-
function followup(sessionId, response) {
|
|
91
|
-
queueMicrotask(() => {
|
|
92
|
-
try {
|
|
93
|
-
audit?.({ id: sessionId }, REFRESH_AUDIT_TYPE, { operation: 'refresh', requestId: response.receipt?.requestId ?? null, revision: response.revision ?? null, selectionRevision: response.selectionRevision ?? null })
|
|
94
|
-
} catch (error) {
|
|
95
|
-
warn(`refresh audit failed: ${message(error)}`)
|
|
96
|
-
}
|
|
97
|
-
})
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
/** @param {unknown} request */
|
|
101
|
-
function validateRequest(request) {
|
|
102
|
-
const input = /** @type {Record<string, unknown>} */ (request ?? {})
|
|
103
|
-
if (typeof input.requestId !== 'string' || !input.requestId.length) return { error: 'invalid-request:requestId' }
|
|
104
|
-
if (!Number.isSafeInteger(input.expectedRevision) || /** @type {number} */ (input.expectedRevision) < 0) return { error: 'invalid-request:expectedRevision' }
|
|
105
|
-
if (!Number.isSafeInteger(input.expectedSelectionRevision) || /** @type {number} */ (input.expectedSelectionRevision) < 0) return { error: 'invalid-request:expectedSelectionRevision' }
|
|
106
|
-
/** @type {{ keys: string[], identities: unknown[] } | null} */
|
|
107
|
-
let skills = null
|
|
108
|
-
if (input.skills !== undefined) {
|
|
109
|
-
if (!Array.isArray(input.skills)) return { error: 'invalid-request:skills' }
|
|
110
|
-
try {
|
|
111
|
-
const byKey = new Map(input.skills.map(raw => {
|
|
112
|
-
const identity = createSkillIdentity(raw)
|
|
113
|
-
return [skillIdentityKey(identity), identity]
|
|
114
|
-
}))
|
|
115
|
-
skills = { keys: [...byKey.keys()].sort(), identities: [...byKey.values()] }
|
|
116
|
-
} catch (error) {
|
|
117
|
-
return { error: `invalid-request:skills:${message(error)}` }
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
return {
|
|
121
|
-
normalized: {
|
|
122
|
-
requestId: input.requestId,
|
|
123
|
-
expectedRevision: /** @type {number} */ (input.expectedRevision),
|
|
124
|
-
expectedSelectionRevision: /** @type {number} */ (input.expectedSelectionRevision),
|
|
125
|
-
skills,
|
|
126
|
-
},
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
/**
|
|
131
|
-
* @param {unknown} authSession - authenticated session handle; the ONLY
|
|
132
|
-
* source of the session location
|
|
133
|
-
* @param {unknown} request - client request; only requestId, the expected
|
|
134
|
-
* revisions and the optional identity subset are read
|
|
135
|
-
* @returns {Promise<RefreshResult>}
|
|
136
|
-
*/
|
|
137
|
-
async function refresh(authSession, request) {
|
|
138
|
-
// Step 1: locate server-side from the authenticated session.
|
|
139
|
-
let located
|
|
140
|
-
try {
|
|
141
|
-
located = await locateSession(authSession)
|
|
142
|
-
} catch (error) {
|
|
143
|
-
return { status: 'rejected', reason: `unauthenticated:${message(error)}`, step: 1 }
|
|
144
|
-
}
|
|
145
|
-
if (located === null || typeof located !== 'object' || !isSegment(located.sessionId)) {
|
|
146
|
-
return { status: 'rejected', reason: 'unauthenticated:session-unavailable', step: 1 }
|
|
147
|
-
}
|
|
148
|
-
const { sessionId } = located
|
|
149
|
-
// The SAME commit coordinator the selection Apply uses.
|
|
150
|
-
return coordinate(sessionId, () => transact(sessionId, located, request))
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
/** @param {string} sessionId @param {{ cwd?: string }} located @param {unknown} request @returns {Promise<RefreshResult>} */
|
|
154
|
-
async function transact(sessionId, located, request) {
|
|
155
|
-
const contentUnit = { kind: 'content', sessionId }
|
|
156
|
-
const selectionUnit = { kind: 'selection', sessionId }
|
|
157
|
-
const viewOptions = { ...(located.cwd === undefined ? {} : { cwd: located.cwd }), scope: { session: { id: sessionId } } }
|
|
158
|
-
|
|
159
|
-
// Step 2: validate the request shape and the exact identity subset.
|
|
160
|
-
const validated = validateRequest(request)
|
|
161
|
-
if (validated.error) return { status: 'rejected', reason: validated.error, step: 2 }
|
|
162
|
-
const { normalized } = validated
|
|
163
|
-
|
|
164
|
-
// Step 3: read the CURRENT selection — the only authority a refresh serves.
|
|
165
|
-
const first = await store.read(selectionUnit)
|
|
166
|
-
if (first.kind === 'unsupported') return { status: 'rejected', reason: `unsupported:${first.reason}`, step: 3 }
|
|
167
|
-
if (first.kind !== 'ok' && first.kind !== 'absent') return { status: 'rejected', reason: `policy-unreadable:${first.kind}`, step: 3 }
|
|
168
|
-
const selectionRevision = first.kind === 'ok' ? first.revision : 0
|
|
169
|
-
trace('selection-read', { sessionId, revision: selectionRevision })
|
|
170
|
-
if (selectionRevision !== normalized.expectedSelectionRevision) {
|
|
171
|
-
trace('selection-revision-changed', { sessionId, from: normalized.expectedSelectionRevision, to: selectionRevision })
|
|
172
|
-
return { status: 'rejected', reason: 'selection-revision-changed', step: 3, current: { revision: selectionRevision } }
|
|
173
|
-
}
|
|
174
|
-
/** @type {Map<string, unknown>} */
|
|
175
|
-
const selected = new Map()
|
|
176
|
-
if (first.kind === 'ok') {
|
|
177
|
-
for (const raw of safeSkills(first.payload)) {
|
|
178
|
-
try {
|
|
179
|
-
const identity = createSkillIdentity(raw)
|
|
180
|
-
selected.set(skillIdentityKey(identity), identity)
|
|
181
|
-
} catch {
|
|
182
|
-
// A durably accepted record holds valid identities; a stray entry is
|
|
183
|
-
// skipped here and stays unrefreshable, never substituted by name.
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
const requested = normalized.skills ? normalized.skills.keys : [...selected.keys()].sort()
|
|
188
|
-
const notSelected = requested.filter(key => !selected.has(key))
|
|
189
|
-
if (notSelected.length) {
|
|
190
|
-
// A removed skill is never re-published by a refresh.
|
|
191
|
-
trace('skill-not-selected', { sessionId, missing: notSelected })
|
|
192
|
-
return { status: 'rejected', reason: 'skill-not-selected', step: 3, missing: notSelected }
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
// Collect and verify fresh content handles server-side; nothing is
|
|
196
|
-
// committed or published here.
|
|
197
|
-
/** @type {Readonly<{ identity: unknown, name: string|null, digest: string|null, path: string|null }>[]} */
|
|
198
|
-
let prepared
|
|
199
|
-
try {
|
|
200
|
-
const snapshot = await collect(viewOptions)
|
|
201
|
-
if (!snapshot || snapshot.complete !== true || !Array.isArray(snapshot.candidates)) {
|
|
202
|
-
return { status: 'rejected', reason: 'inventory-incomplete', step: 3 }
|
|
203
|
-
}
|
|
204
|
-
const parsed = snapshot.candidates.filter(candidate => candidate?.status === 'parsed' && candidate.identity)
|
|
205
|
-
/** @type {Map<string, unknown[]>} */
|
|
206
|
-
const byKey = new Map()
|
|
207
|
-
for (const candidate of parsed) {
|
|
208
|
-
const key = skillIdentityKey(candidate.identity)
|
|
209
|
-
byKey.set(key, [...(byKey.get(key) ?? []), candidate])
|
|
210
|
-
}
|
|
211
|
-
const missing = requested.filter(key => (byKey.get(key) ?? []).length !== 1)
|
|
212
|
-
if (missing.length) return { status: 'rejected', reason: 'selected-identity-missing', step: 3, missing }
|
|
213
|
-
const matched = requested.map(key => /** @type {unknown[]} */ (byKey.get(key))[0])
|
|
214
|
-
if (verifyContent) for (const candidate of matched) await verifyContent(candidate)
|
|
215
|
-
prepared = matched.map(candidate => Object.freeze({
|
|
216
|
-
identity: candidate.identity,
|
|
217
|
-
name: candidate.name ?? null,
|
|
218
|
-
digest: candidate.digest ?? null,
|
|
219
|
-
path: candidate.path ?? null,
|
|
220
|
-
}))
|
|
221
|
-
} catch (error) {
|
|
222
|
-
return { status: 'rejected', reason: `preparation-failed:${message(error)}`, step: 3 }
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
// Step 3 (final): RE-CHECK the selection revision immediately before the
|
|
226
|
-
// commit. Inside the shared coordinator no in-process Apply can
|
|
227
|
-
// interleave; against a cross-process writer this is the last durable
|
|
228
|
-
// look — the invariant is that a skill removed AT COMMIT TIME is never
|
|
229
|
-
// published by this refresh.
|
|
230
|
-
const settled = await store.read(selectionUnit)
|
|
231
|
-
if (settled.kind === 'unsupported') return { status: 'rejected', reason: `unsupported:${settled.reason}`, step: 3 }
|
|
232
|
-
if (settled.kind !== 'ok' && settled.kind !== 'absent') return { status: 'rejected', reason: `policy-unreadable:${settled.kind}`, step: 3 }
|
|
233
|
-
const settledRevision = settled.kind === 'ok' ? settled.revision : 0
|
|
234
|
-
trace('selection-recheck', { sessionId, revision: settledRevision })
|
|
235
|
-
if (settledRevision !== selectionRevision) {
|
|
236
|
-
trace('selection-revision-changed', { sessionId, from: selectionRevision, to: settledRevision })
|
|
237
|
-
return { status: 'rejected', reason: 'selection-revision-changed', step: 3, current: { revision: settledRevision } }
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
// Step 4: durable CAS write through the group-2 store; the content unit
|
|
241
|
-
// owns its revision and receipts independently of the selection unit.
|
|
242
|
-
const payload = { contents: prepared, selectionRevision, origin: 'refresh' }
|
|
243
|
-
const requestDigest = digestRefreshRequest({
|
|
244
|
-
expectedRevision: normalized.expectedRevision,
|
|
245
|
-
expectedSelectionRevision: normalized.expectedSelectionRevision,
|
|
246
|
-
skills: normalized.skills ? normalized.skills.keys : null,
|
|
247
|
-
})
|
|
248
|
-
let result
|
|
249
|
-
try {
|
|
250
|
-
result = await store.commit(contentUnit, normalized.expectedRevision, () => payload, { requestId: normalized.requestId, requestDigest })
|
|
251
|
-
} catch {
|
|
252
|
-
// The write outcome is UNKNOWN: nothing is held (no fence, no in-memory
|
|
253
|
-
// publication), so a receipt query settles it from durable evidence.
|
|
254
|
-
trace('indeterminate', { sessionId })
|
|
255
|
-
return { status: 'indeterminate', step: 4 }
|
|
256
|
-
}
|
|
257
|
-
trace('commit', { sessionId, status: result.status })
|
|
258
|
-
|
|
259
|
-
switch (result.status) {
|
|
260
|
-
case 'committed': {
|
|
261
|
-
const response = { status: /** @type {const} */ ('refreshed'), revision: result.revision, receipt: result.receipt, selectionRevision, contents: structuredClone(prepared) }
|
|
262
|
-
trace('response', { sessionId, revision: result.revision })
|
|
263
|
-
followup(sessionId, response)
|
|
264
|
-
return response
|
|
265
|
-
}
|
|
266
|
-
case 'duplicate': {
|
|
267
|
-
// The same accepted request: return the original receipt, never a
|
|
268
|
-
// second commit. Re-read so revision and marker come from the SAME
|
|
269
|
-
// settled record the read actually observed.
|
|
270
|
-
const settledRecord = await store.read(contentUnit).catch(() => null)
|
|
271
|
-
const receipt = /** @type {{ requestId: string, requestDigest: string }} */ (result.receipt)
|
|
272
|
-
const evidenced = settledRecord?.kind === 'ok'
|
|
273
|
-
&& settledRecord.receipts.some(entry => entry.requestId === receipt.requestId && entry.requestDigest === receipt.requestDigest)
|
|
274
|
-
if (evidenced && settledRecord?.kind === 'ok') {
|
|
275
|
-
const settledPayload = /** @type {Record<string, unknown>} */ (settledRecord.payload ?? {})
|
|
276
|
-
return { status: 'duplicate', revision: settledRecord.revision, receipt: result.receipt, selectionRevision: settledPayload.selectionRevision }
|
|
277
|
-
}
|
|
278
|
-
return { status: 'duplicate', revision: result.revision, receipt: result.receipt, warnings: ['settlement-unverified'] }
|
|
279
|
-
}
|
|
280
|
-
case 'request-conflict':
|
|
281
|
-
return { status: 'rejected', reason: 'request-conflict', step: 4, receipt: result.receipt }
|
|
282
|
-
case 'revision-conflict': {
|
|
283
|
-
const current = await store.read(contentUnit).catch(() => null)
|
|
284
|
-
return { status: 'rejected', reason: 'revision-conflict', step: 4, current: current?.kind === 'ok' ? { revision: current.revision } : { revision: result.revision } }
|
|
285
|
-
}
|
|
286
|
-
case 'locked':
|
|
287
|
-
return { status: 'rejected', reason: `locked:${result.reason}`, step: 4 }
|
|
288
|
-
case 'unreadable':
|
|
289
|
-
return { status: 'rejected', reason: `policy-unreadable:${result.kind}`, step: 4 }
|
|
290
|
-
case 'unsupported':
|
|
291
|
-
return { status: 'rejected', reason: `unsupported:${result.reason}`, step: 4 }
|
|
292
|
-
case 'write-failed':
|
|
293
|
-
trace('write-failed', { sessionId })
|
|
294
|
-
return { status: 'write-failed', step: 4 }
|
|
295
|
-
default:
|
|
296
|
-
// Unknown store outcome: nothing was held, report indeterminate.
|
|
297
|
-
trace('indeterminate', { sessionId, status: result.status })
|
|
298
|
-
return { status: 'indeterminate', step: 4 }
|
|
299
|
-
}
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
/**
|
|
303
|
-
* Receipt query on the CONTENT unit: after a lost response or an
|
|
304
|
-
* indeterminate write, the client queries the original requestId instead of
|
|
305
|
-
* claiming a cancellation or a rollback that never happened.
|
|
306
|
-
* @param {unknown} authSession @param {string} requestId
|
|
307
|
-
* @returns {Promise<RefreshReceiptQueryResult>}
|
|
308
|
-
*/
|
|
309
|
-
async function queryReceipt(authSession, requestId) {
|
|
310
|
-
if (typeof requestId !== 'string' || !requestId.length) return { status: 'rejected', reason: 'invalid-request:requestId' }
|
|
311
|
-
let located
|
|
312
|
-
try {
|
|
313
|
-
located = await locateSession(authSession)
|
|
314
|
-
} catch (error) {
|
|
315
|
-
return { status: 'rejected', reason: `unauthenticated:${message(error)}` }
|
|
316
|
-
}
|
|
317
|
-
if (located === null || typeof located !== 'object' || !isSegment(located.sessionId)) {
|
|
318
|
-
return { status: 'rejected', reason: 'unauthenticated:session-unavailable' }
|
|
319
|
-
}
|
|
320
|
-
const record = await store.read({ kind: 'content', sessionId: located.sessionId })
|
|
321
|
-
if (record.kind === 'unsupported') return { status: 'rejected', reason: `unsupported:${record.reason}` }
|
|
322
|
-
if (record.kind !== 'ok') return { status: 'not-found', revision: record.kind === 'absent' ? 0 : null }
|
|
323
|
-
const payload = /** @type {Record<string, unknown>} */ (record.payload ?? {})
|
|
324
|
-
const receipt = record.receipts.find(entry => entry.requestId === requestId)
|
|
325
|
-
return receipt
|
|
326
|
-
? { status: 'found', revision: record.revision, receipt, selectionRevision: payload.selectionRevision, contents: Array.isArray(payload.contents) ? payload.contents : [] }
|
|
327
|
-
: { status: 'not-found', revision: record.revision }
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
return { refresh, queryReceipt }
|
|
331
|
-
}
|
|
@@ -1,78 +0,0 @@
|
|
|
1
|
-
// Task 10.1 of the session-capability-manager change: the distribution-side
|
|
2
|
-
// input manifest and provenance records. Every entry carries the source
|
|
3
|
-
// repository, source kind, requested ref, subpath, install location and
|
|
4
|
-
// content identity (reusing the group-3 raw inventory). Values that cannot
|
|
5
|
-
// be established are reported as `unknown` — never inferred, never
|
|
6
|
-
// fabricated, and a missing identity is never claimed as "unchanged".
|
|
7
|
-
// Entries that are unavailable, name-conflicting or locally modified stay
|
|
8
|
-
// listed (with their state), never silently dropped.
|
|
9
|
-
|
|
10
|
-
export const DISTRIBUTION_SCHEMA_VERSION = 1
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* @typedef {{ repository: string, sourceKind: string, requestedRef: string, subpath: string|null,
|
|
14
|
-
* installLocation: string, contentIdentity: string|null,
|
|
15
|
-
* state: 'ok'|'unavailable'|'name-conflict'|'locally-modified'|'unknown' }} DistributionEntry
|
|
16
|
-
*/
|
|
17
|
-
|
|
18
|
-
/** @param {unknown} value */
|
|
19
|
-
const isNonEmptyString = value => typeof value === 'string' && value.length > 0
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
* Normalize one raw record into a manifest entry. Anything not established
|
|
23
|
-
* by the record becomes 'unknown' verbatim — no inference, no comparison.
|
|
24
|
-
* @param {Record<string, unknown>} raw
|
|
25
|
-
* @returns {DistributionEntry}
|
|
26
|
-
*/
|
|
27
|
-
export function manifestEntryOf(raw) {
|
|
28
|
-
const unknown = { repository: 'unknown', sourceKind: 'unknown', requestedRef: 'unknown', subpath: null, installLocation: 'unknown', contentIdentity: null, state: 'unknown' }
|
|
29
|
-
if (raw === null || typeof raw !== 'object' || Array.isArray(raw)) return unknown
|
|
30
|
-
return {
|
|
31
|
-
repository: isNonEmptyString(raw.repository) ? raw.repository : 'unknown',
|
|
32
|
-
sourceKind: isNonEmptyString(raw.sourceKind) ? raw.sourceKind : 'unknown',
|
|
33
|
-
requestedRef: isNonEmptyString(raw.requestedRef) ? raw.requestedRef : 'unknown',
|
|
34
|
-
subpath: typeof raw.subpath === 'string' ? raw.subpath : null,
|
|
35
|
-
installLocation: isNonEmptyString(raw.installLocation) ? raw.installLocation : 'unknown',
|
|
36
|
-
contentIdentity: isNonEmptyString(raw.contentIdentity) ? raw.contentIdentity : null,
|
|
37
|
-
state: ['ok', 'unavailable', 'name-conflict', 'locally-modified'].includes(raw.state) ? raw.state : 'unknown',
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
/**
|
|
42
|
-
* Compare is only defined when BOTH identities are established; a missing
|
|
43
|
-
* identity is never claimed as "unchanged" — it reports as unknown.
|
|
44
|
-
* @param {DistributionEntry} entry @param {string|null} currentIdentity
|
|
45
|
-
*/
|
|
46
|
-
export function changeVerdict(entry, currentIdentity) {
|
|
47
|
-
if (entry.contentIdentity === null || typeof currentIdentity !== 'string' || currentIdentity.length === 0) {
|
|
48
|
-
return 'unknown'
|
|
49
|
-
}
|
|
50
|
-
return entry.contentIdentity === currentIdentity ? 'unchanged' : 'changed'
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
// Task 10.2 of the session-capability-manager change: the bundle-owned
|
|
54
|
-
// builtin guard. Builtin skills are delivered and updated ONLY through the
|
|
55
|
-
// bundle's own workflow; no third-party process may write, update, move or
|
|
56
|
-
// delete their content, and a same-named third-party skill is never a valid
|
|
57
|
-
// definition of a builtin identity.
|
|
58
|
-
|
|
59
|
-
/**
|
|
60
|
-
* Whether a target location falls inside the Weir builtin skills root —
|
|
61
|
-
* distribution writes there are always refused (10.2).
|
|
62
|
-
* @param {string} targetPath @param {string} builtinRoot
|
|
63
|
-
*/
|
|
64
|
-
export function isBuiltinTarget(targetPath, builtinRoot) {
|
|
65
|
-
if (typeof targetPath !== 'string' || typeof builtinRoot !== 'string') return false
|
|
66
|
-
const root = builtinRoot.endsWith('/') ? builtinRoot : `${builtinRoot}/`
|
|
67
|
-
return targetPath === builtinRoot || targetPath.startsWith(root)
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
/**
|
|
71
|
-
* Whether a candidate may serve as the definition of a builtin identity: a
|
|
72
|
-
* third-party same-name candidate is NEVER valid for a builtin identity
|
|
73
|
-
* (10.2).
|
|
74
|
-
* @param {{ scope?: string, provider?: string }} candidate
|
|
75
|
-
*/
|
|
76
|
-
export function isValidBuiltinDefinition(candidate) {
|
|
77
|
-
return candidate?.scope === 'weir-builtin'
|
|
78
|
-
}
|
|
@@ -1,106 +0,0 @@
|
|
|
1
|
-
// Tasks 10.4/10.5 of the session-capability-manager change: the pinned
|
|
2
|
-
// executor baseline and the isolated execution layer for managed external
|
|
3
|
-
// installs/updates.
|
|
4
|
-
//
|
|
5
|
-
// 10.4 — the managed external executable is `vercel-labs/skills` pinned at
|
|
6
|
-
// version 1.7.0 / commit 3694740352eeef5cdd689af694c485f1ff62eec3. It is
|
|
7
|
-
// never a bundle npm dependency; the source, artifact integrity and the
|
|
8
|
-
// ACTUAL version are recorded, the revision in use is reported in every
|
|
9
|
-
// diagnostic, and any deviation from the pin is reported and REFUSES the
|
|
10
|
-
// managed install/update until a compatibility test passes with explicit
|
|
11
|
-
// user authorization.
|
|
12
|
-
//
|
|
13
|
-
// 10.5 — isolation: a private staging HOME/XDG/cwd, explicit non-interactive
|
|
14
|
-
// argv (never shell-concatenated from source text), `shell: false`,
|
|
15
|
-
// telemetry off, isolated user git config, no Git hooks / external diff or
|
|
16
|
-
// filter drivers, no source scripts executed. One operation never exceeds
|
|
17
|
-
// 5 minutes or 4 MiB of stdout+stderr combined; breaching either terminates
|
|
18
|
-
// the whole process tree, redacts diagnostics and records the failure.
|
|
19
|
-
|
|
20
|
-
export const EXECUTOR_PIN = Object.freeze({
|
|
21
|
-
repository: 'vercel-labs/skills',
|
|
22
|
-
version: '1.7.0',
|
|
23
|
-
commit: '3694740352eeef5cdd689af694c485f1ff62eec3',
|
|
24
|
-
})
|
|
25
|
-
|
|
26
|
-
export const EXEC_LIMITS = Object.freeze({ timeoutMs: 300_000, maxOutputBytes: 4_194_304 })
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* The baseline verdict for one executor instance (10.4): exact pin match
|
|
30
|
-
* runs; any deviation reports and refuses.
|
|
31
|
-
* @param {{ version?: string, commit?: string }} actual
|
|
32
|
-
* @returns {{ run: boolean, revision: string, deviation: string|null }}
|
|
33
|
-
*/
|
|
34
|
-
export function pinVerdict(actual) {
|
|
35
|
-
const revision = actual?.commit ?? actual?.version ?? 'unknown'
|
|
36
|
-
if (actual?.version !== EXECUTOR_PIN.version || actual?.commit !== EXECUTOR_PIN.commit) {
|
|
37
|
-
return {
|
|
38
|
-
run: false,
|
|
39
|
-
revision,
|
|
40
|
-
deviation: `executor baseline deviation: expected ${EXECUTOR_PIN.repository}@${EXECUTOR_PIN.version} (${EXECUTOR_PIN.commit}), got ${actual?.version ?? 'unknown'} (${actual?.commit ?? 'unknown'}); managed install/update is refused until a compatibility test passes with user authorization`,
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
return { run: true, revision: EXECUTOR_PIN.commit, deviation: null }
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
/**
|
|
47
|
-
* The non-interactive argv for one managed install (10.5). Arguments are
|
|
48
|
-
* passed VERBATIM (paths and refs with spaces stay single array items);
|
|
49
|
-
* nothing from source text is ever concatenated into a shell string.
|
|
50
|
-
* @param {{ repository: string, skill: string, agent?: string, yes?: never }} input
|
|
51
|
-
*/
|
|
52
|
-
export function installArgv({ repository, skill, agent = 'universal' }) {
|
|
53
|
-
for (const [label, value] of [['repository', repository], ['skill', skill], ['agent', agent]]) {
|
|
54
|
-
if (typeof value !== 'string' || value.length === 0) throw new TypeError(`install argv needs a non-empty ${label}`)
|
|
55
|
-
}
|
|
56
|
-
return ['skills', 'add', repository, '--skill', skill, '--agent', agent, '--no-interactive']
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
/**
|
|
60
|
-
* The isolated env for one managed invocation (10.5): private staging
|
|
61
|
-
* HOME/XDG, telemetry off, git noise silenced.
|
|
62
|
-
* @param {{ stagingHome: string }} input
|
|
63
|
-
*/
|
|
64
|
-
export function isolatedEnv({ stagingHome }) {
|
|
65
|
-
if (typeof stagingHome !== 'string' || stagingHome.length === 0) throw new TypeError('a staging home is required')
|
|
66
|
-
return {
|
|
67
|
-
HOME: stagingHome,
|
|
68
|
-
XDG_CONFIG_HOME: stagingHome,
|
|
69
|
-
XDG_CACHE_HOME: stagingHome,
|
|
70
|
-
XDG_DATA_HOME: stagingHome,
|
|
71
|
-
XDG_STATE_HOME: stagingHome,
|
|
72
|
-
TMPDIR: stagingHome,
|
|
73
|
-
SKILLS_TELEMETRY: '0',
|
|
74
|
-
GIT_CONFIG_NOSYSTEM: '1',
|
|
75
|
-
GIT_CONFIG_GLOBAL: '/dev/null',
|
|
76
|
-
GIT_TERMINAL_PROMPT: '0',
|
|
77
|
-
GIT_HOOKS_PATH: '/dev/null',
|
|
78
|
-
GIT_EXTERNAL_DIFF: '',
|
|
79
|
-
GIT_DIFF_DRIVER: '',
|
|
80
|
-
GIT_FILTER_BRANCH_COMMAND: '',
|
|
81
|
-
CI: '1',
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
/** The spawn options for one managed invocation (10.5). */
|
|
86
|
-
export function spawnOptionsFor({ stagingHome }) {
|
|
87
|
-
return { argv0: 'skills', env: isolatedEnv({ stagingHome }), cwd: stagingHome, shell: false, timeout: EXEC_LIMITS.timeoutMs, maxBuffer: EXEC_LIMITS.maxOutputBytes }
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
/**
|
|
91
|
-
* The diagnostics for one run (10.4/10.5): the revision in use is always
|
|
92
|
-
* reported, and secrets are redacted from any echo.
|
|
93
|
-
* @param {{ revision: string, argv: string[], code: number|null, stdout: string, stderr: string, timedOut?: boolean, overOutput?: boolean }} run
|
|
94
|
-
*/
|
|
95
|
-
export function runDiagnostics({ revision, argv, code, stdout, stderr, timedOut = false, overOutput = false }) {
|
|
96
|
-
const redact = text => String(text).replace(/(token|secret|password|key)[=:]\s*\S+/gi, '$1=<redacted>')
|
|
97
|
-
return {
|
|
98
|
-
revision,
|
|
99
|
-
argv: [...argv],
|
|
100
|
-
code,
|
|
101
|
-
timedOut,
|
|
102
|
-
overOutput,
|
|
103
|
-
stdout: redact(stdout).slice(0, 2000),
|
|
104
|
-
stderr: redact(stderr).slice(0, 2000),
|
|
105
|
-
}
|
|
106
|
-
}
|