@kylecheng3146/agent-ops 0.1.4 → 0.1.6
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 +164 -13
- package/dist/packages/cli/src/args.js +40 -7
- package/dist/packages/cli/src/bin.js +35 -11
- package/dist/packages/cli/src/cli.js +7 -2
- package/dist/packages/cli/src/codex-loop-process.js +70 -0
- package/dist/packages/cli/src/commands/doctor.js +31 -9
- package/dist/packages/cli/src/commands/hook.js +33 -16
- package/dist/packages/cli/src/commands/init.js +9 -5
- package/dist/packages/cli/src/commands/review.js +5 -1
- package/dist/packages/cli/src/commands/uninstall.js +6 -5
- package/dist/packages/cli/src/commands/update.js +16 -5
- package/dist/packages/cli/src/context.js +69 -3
- package/dist/packages/cli/src/hook-process.js +231 -16
- package/dist/packages/cli/src/loop-entry.js +8 -0
- package/dist/packages/cli/src/plan-output.js +9 -4
- package/dist/packages/cli/src/public-plan.js +62 -0
- package/dist/packages/cli/src/version.js +1 -1
- package/dist/packages/cli/src/wizard.js +36 -15
- package/dist/runtime/src/adapters/claude/config.js +57 -19
- package/dist/runtime/src/adapters/claude/events.js +33 -0
- package/dist/runtime/src/adapters/claude/output.js +8 -7
- package/dist/runtime/src/adapters/claude/surfaces.js +70 -0
- package/dist/runtime/src/adapters/codex/config.js +66 -13
- package/dist/runtime/src/adapters/codex/events.js +33 -0
- package/dist/runtime/src/adapters/codex/output.js +10 -0
- package/dist/runtime/src/adapters/codex/surfaces.js +12 -0
- package/dist/runtime/src/adapters/opencode/config.js +170 -0
- package/dist/runtime/src/adapters/opencode/events.js +49 -0
- package/dist/runtime/src/adapters/opencode/input.js +32 -0
- package/dist/runtime/src/adapters/opencode/output.js +23 -0
- package/dist/runtime/src/adapters/opencode/surfaces.js +23 -0
- package/dist/runtime/src/config/explain.js +7 -0
- package/dist/runtime/src/config/hash.js +24 -0
- package/dist/runtime/src/config/merge.js +6 -2
- package/dist/runtime/src/config/migrate.js +19 -4
- package/dist/runtime/src/contracts.js +10 -1
- package/dist/runtime/src/fs/managed-block.js +35 -18
- package/dist/runtime/src/fs/manifest.js +32 -1
- package/dist/runtime/src/fs/transaction.js +14 -2
- package/dist/runtime/src/hooks/advisory.js +16 -0
- package/dist/runtime/src/hooks/codex-loop.js +439 -0
- package/dist/runtime/src/hooks/stop-service.js +70 -0
- package/dist/runtime/src/hooks/stop-verify.js +4 -1
- package/dist/runtime/src/install/codex-loop.js +139 -0
- package/dist/runtime/src/install/doctor.js +182 -14
- package/dist/runtime/src/install/harness.js +294 -35
- package/dist/runtime/src/install/hooks.js +22 -17
- package/dist/runtime/src/install/ownership.js +146 -35
- package/dist/runtime/src/install/plan.js +276 -31
- package/dist/runtime/src/install/probes.js +9 -43
- package/dist/runtime/src/install/profiles.js +12 -3
- package/dist/runtime/src/install/surface-inspection.js +296 -0
- package/dist/runtime/src/install/surfaces.js +11 -0
- package/dist/runtime/src/install/uninstall.js +11 -4
- package/dist/runtime/src/install/update.js +13 -3
- package/dist/runtime/src/logging/local-log.js +25 -0
- package/dist/runtime/src/schema/validate.js +45 -19
- package/dist/runtime/src/task/service.js +3 -3
- package/dist/runtime/src/task/store.js +12 -3
- package/dist/runtime/src/verify/command-executor.js +113 -0
- package/dist/runtime/src/verify/evidence.js +4 -24
- package/dist/runtime/src/verify/service.js +20 -92
- package/dist/runtime/src/verify/spawn.js +6 -1
- package/docs/en/guides/configuration.md +159 -0
- package/docs/en/guides/quickstart.md +9 -0
- package/docs/en/guides/security.md +5 -0
- package/docs/en/spec/README.md +9 -0
- package/docs/en/spec/harness-adapters.md +104 -2
- package/docs/en/spec/maintenance.md +11 -0
- package/docs/en/spec/review.md +11 -0
- package/docs/zh-TW/guides/configuration.md +145 -0
- package/docs/zh-TW/guides/quickstart.md +9 -0
- package/docs/zh-TW/guides/security.md +5 -0
- package/docs/zh-TW/spec/README.md +9 -0
- package/docs/zh-TW/spec/harness-adapters.md +89 -3
- package/docs/zh-TW/spec/maintenance.md +11 -1
- package/docs/zh-TW/spec/review.md +10 -0
- package/package.json +4 -2
- package/schemas/config.schema.json +56 -2
- package/schemas/manifest.schema.json +19 -3
- package/templates/common/AGENTS.block.md +2 -1
- package/templates/common/CLAUDE.block.md +2 -1
- package/dist/runtime/src/review/claude-runner.js +0 -4
- package/dist/runtime/src/review/codex-runner.js +0 -4
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { runVerificationCommand } from "./spawn.js";
|
|
2
|
+
import { evaluateTestCount, parseTestCount } from "./test-count.js";
|
|
3
|
+
function classifyTestCountCode(code) {
|
|
4
|
+
const classes = {
|
|
5
|
+
TEST_COUNT_BELOW_MINIMUM: "test-count-below-minimum",
|
|
6
|
+
TEST_COUNT_INVALID: "test-count-invalid",
|
|
7
|
+
TEST_COUNT_OK: "none",
|
|
8
|
+
TEST_COUNT_REQUIREMENT_INVALID: "test-count-requirement-invalid",
|
|
9
|
+
TEST_COUNT_UNPARSEABLE: "test-count-unparseable",
|
|
10
|
+
ZERO_TESTS: "zero-tests"
|
|
11
|
+
};
|
|
12
|
+
return classes[code];
|
|
13
|
+
}
|
|
14
|
+
function untrustedResult(commandId) {
|
|
15
|
+
return {
|
|
16
|
+
commandId,
|
|
17
|
+
status: "UNKNOWN",
|
|
18
|
+
failureClass: "repository-untrusted",
|
|
19
|
+
exitCode: null,
|
|
20
|
+
signal: null,
|
|
21
|
+
timedOut: false,
|
|
22
|
+
durationMs: 0,
|
|
23
|
+
stdout: "",
|
|
24
|
+
stderr: "",
|
|
25
|
+
stdoutTruncated: false,
|
|
26
|
+
stderrTruncated: false
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
function classifyCommand(command, spawned) {
|
|
30
|
+
if (command.evidence.kind === "file") {
|
|
31
|
+
return {
|
|
32
|
+
status: spawned.status === "PASS" ? "UNKNOWN" : spawned.status,
|
|
33
|
+
failureClass: spawned.status === "PASS"
|
|
34
|
+
? "file-evidence-unsupported"
|
|
35
|
+
: spawned.failureClass,
|
|
36
|
+
testCount: null
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
if (command.evidence.kind !== "test-count") {
|
|
40
|
+
return {
|
|
41
|
+
status: spawned.status,
|
|
42
|
+
failureClass: spawned.failureClass,
|
|
43
|
+
testCount: null
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
const testCount = parseTestCount(`${spawned.stdout}\n${spawned.stderr}`);
|
|
47
|
+
if (spawned.status !== "PASS") {
|
|
48
|
+
return {
|
|
49
|
+
status: spawned.status,
|
|
50
|
+
failureClass: spawned.failureClass,
|
|
51
|
+
testCount
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
const evaluation = evaluateTestCount(testCount, command.evidence.minimum);
|
|
55
|
+
return {
|
|
56
|
+
status: evaluation.status,
|
|
57
|
+
failureClass: classifyTestCountCode(evaluation.code),
|
|
58
|
+
testCount: evaluation.testCount
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
function diagnostic(spawned) {
|
|
62
|
+
return spawned.stderr || spawned.stdout || spawned.failureClass;
|
|
63
|
+
}
|
|
64
|
+
export async function executeConfiguredCommand(command, options) {
|
|
65
|
+
let spawned;
|
|
66
|
+
if (!options.trusted) {
|
|
67
|
+
spawned = untrustedResult(command.id);
|
|
68
|
+
}
|
|
69
|
+
else {
|
|
70
|
+
const runOptions = {
|
|
71
|
+
cwd: options.cwd,
|
|
72
|
+
...(options.runner === undefined
|
|
73
|
+
? {}
|
|
74
|
+
: { runner: options.runner }),
|
|
75
|
+
...(options.env === undefined ? {} : { env: options.env }),
|
|
76
|
+
...(options.now === undefined ? {} : { now: options.now }),
|
|
77
|
+
...(options.outputLimitBytes === undefined
|
|
78
|
+
? {}
|
|
79
|
+
: { outputLimitBytes: options.outputLimitBytes }),
|
|
80
|
+
...(options.terminationGraceMs === undefined
|
|
81
|
+
? {}
|
|
82
|
+
: { terminationGraceMs: options.terminationGraceMs })
|
|
83
|
+
};
|
|
84
|
+
spawned = await runVerificationCommand(command, runOptions);
|
|
85
|
+
}
|
|
86
|
+
const classified = classifyCommand(command, spawned);
|
|
87
|
+
return {
|
|
88
|
+
commandId: command.id,
|
|
89
|
+
required: command.required,
|
|
90
|
+
status: classified.status,
|
|
91
|
+
failureClass: classified.failureClass,
|
|
92
|
+
exitCode: spawned.exitCode,
|
|
93
|
+
signal: spawned.signal,
|
|
94
|
+
timedOut: spawned.timedOut,
|
|
95
|
+
testCount: classified.testCount,
|
|
96
|
+
diagnostic: diagnostic(spawned),
|
|
97
|
+
stdout: spawned.stdout,
|
|
98
|
+
stderr: spawned.stderr,
|
|
99
|
+
stdoutTruncated: spawned.stdoutTruncated,
|
|
100
|
+
stderrTruncated: spawned.stderrTruncated
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
export function aggregateVerificationStatus(results) {
|
|
104
|
+
const required = results.filter((result) => result.required);
|
|
105
|
+
const gating = required.length > 0 ? required : results;
|
|
106
|
+
if (gating.some((result) => result.status === "FAIL")) {
|
|
107
|
+
return "FAIL";
|
|
108
|
+
}
|
|
109
|
+
if (gating.some((result) => result.status === "UNKNOWN")) {
|
|
110
|
+
return "UNKNOWN";
|
|
111
|
+
}
|
|
112
|
+
return "PASS";
|
|
113
|
+
}
|
|
@@ -1,32 +1,12 @@
|
|
|
1
1
|
import { join } from "node:path";
|
|
2
|
-
import {
|
|
2
|
+
import { EVIDENCE_SCHEMA_VERSION } from "../contracts.js";
|
|
3
3
|
import { sha256 } from "../fs/hash.js";
|
|
4
4
|
import { AgentOpsError } from "../fs/paths.js";
|
|
5
|
+
import { calculateConfigHash } from "../config/hash.js";
|
|
6
|
+
export { calculateConfigHash } from "../config/hash.js";
|
|
5
7
|
import { validateEvidence } from "../schema/validate.js";
|
|
6
8
|
import { readPrivateFile, writePrivateFile } from "../security/permissions.js";
|
|
7
9
|
import { redactSecrets } from "../security/redact.js";
|
|
8
|
-
function canonicalJson(value) {
|
|
9
|
-
if (value === null ||
|
|
10
|
-
typeof value === "boolean" ||
|
|
11
|
-
typeof value === "number" ||
|
|
12
|
-
typeof value === "string") {
|
|
13
|
-
return JSON.stringify(value);
|
|
14
|
-
}
|
|
15
|
-
if (Array.isArray(value)) {
|
|
16
|
-
return `[${value.map(canonicalJson).join(",")}]`;
|
|
17
|
-
}
|
|
18
|
-
if (typeof value === "object") {
|
|
19
|
-
const record = value;
|
|
20
|
-
return `{${Object.keys(record)
|
|
21
|
-
.sort()
|
|
22
|
-
.map((key) => `${JSON.stringify(key)}:${canonicalJson(record[key])}`)
|
|
23
|
-
.join(",")}}`;
|
|
24
|
-
}
|
|
25
|
-
throw new AgentOpsError("CONFIG_HASH_INVALID", "Configuration contains an unsupported value.");
|
|
26
|
-
}
|
|
27
|
-
export function calculateConfigHash(config) {
|
|
28
|
-
return sha256(canonicalJson(config));
|
|
29
|
-
}
|
|
30
10
|
function redactRecord(record) {
|
|
31
11
|
return Object.fromEntries(Object.entries(record).map(([key, value]) => [
|
|
32
12
|
redactSecrets(key),
|
|
@@ -43,7 +23,7 @@ function validateBuiltEvidence(value) {
|
|
|
43
23
|
}
|
|
44
24
|
export function buildVerificationEvidence(input) {
|
|
45
25
|
return validateBuiltEvidence({
|
|
46
|
-
schemaVersion:
|
|
26
|
+
schemaVersion: EVIDENCE_SCHEMA_VERSION,
|
|
47
27
|
taskId: input.taskId,
|
|
48
28
|
criterionId: input.criterionId,
|
|
49
29
|
commandId: input.command.id,
|
|
@@ -3,55 +3,11 @@ import { validateTaskAgainstConfig } from "../schema/validate.js";
|
|
|
3
3
|
import { collectChangeSurface } from "./change-surface.js";
|
|
4
4
|
import { buildVerificationEvidence } from "./evidence.js";
|
|
5
5
|
import { createFailureFingerprint } from "./fingerprint.js";
|
|
6
|
+
import { aggregateVerificationStatus, executeConfiguredCommand } from "./command-executor.js";
|
|
6
7
|
import { selectVerificationScope } from "./scope.js";
|
|
7
|
-
import { runVerificationCommand } from "./spawn.js";
|
|
8
|
-
import { evaluateTestCount, parseTestCount } from "./test-count.js";
|
|
9
8
|
function verificationError(code, message) {
|
|
10
9
|
return new AgentOpsError(code, message);
|
|
11
10
|
}
|
|
12
|
-
function classifyTestCountCode(code) {
|
|
13
|
-
const classes = {
|
|
14
|
-
TEST_COUNT_BELOW_MINIMUM: "test-count-below-minimum",
|
|
15
|
-
TEST_COUNT_INVALID: "test-count-invalid",
|
|
16
|
-
TEST_COUNT_OK: "none",
|
|
17
|
-
TEST_COUNT_REQUIREMENT_INVALID: "test-count-requirement-invalid",
|
|
18
|
-
TEST_COUNT_UNPARSEABLE: "test-count-unparseable",
|
|
19
|
-
ZERO_TESTS: "zero-tests"
|
|
20
|
-
};
|
|
21
|
-
return classes[code];
|
|
22
|
-
}
|
|
23
|
-
function classifyResult(command, spawned) {
|
|
24
|
-
if (command.evidence.kind === "file") {
|
|
25
|
-
return {
|
|
26
|
-
status: spawned.status === "PASS" ? "UNKNOWN" : spawned.status,
|
|
27
|
-
failureClass: spawned.status === "PASS"
|
|
28
|
-
? "file-evidence-unsupported"
|
|
29
|
-
: spawned.failureClass,
|
|
30
|
-
testCount: null
|
|
31
|
-
};
|
|
32
|
-
}
|
|
33
|
-
if (command.evidence.kind !== "test-count") {
|
|
34
|
-
return {
|
|
35
|
-
status: spawned.status,
|
|
36
|
-
failureClass: spawned.failureClass,
|
|
37
|
-
testCount: null
|
|
38
|
-
};
|
|
39
|
-
}
|
|
40
|
-
const testCount = parseTestCount(`${spawned.stdout}\n${spawned.stderr}`);
|
|
41
|
-
if (spawned.status !== "PASS") {
|
|
42
|
-
return {
|
|
43
|
-
status: spawned.status,
|
|
44
|
-
failureClass: spawned.failureClass,
|
|
45
|
-
testCount
|
|
46
|
-
};
|
|
47
|
-
}
|
|
48
|
-
const evaluation = evaluateTestCount(testCount, command.evidence.minimum);
|
|
49
|
-
return {
|
|
50
|
-
status: evaluation.status,
|
|
51
|
-
failureClass: classifyTestCountCode(evaluation.code),
|
|
52
|
-
testCount: evaluation.testCount
|
|
53
|
-
};
|
|
54
|
-
}
|
|
55
11
|
function exitCategory(result) {
|
|
56
12
|
if (result.timedOut) {
|
|
57
13
|
return "timeout";
|
|
@@ -64,17 +20,6 @@ function exitCategory(result) {
|
|
|
64
20
|
}
|
|
65
21
|
return result.exitCode === 0 ? "exit-zero" : "nonzero-exit";
|
|
66
22
|
}
|
|
67
|
-
function overallStatus(results) {
|
|
68
|
-
const required = results.filter((result) => result.required);
|
|
69
|
-
const gating = required.length > 0 ? required : results;
|
|
70
|
-
if (gating.some((result) => result.status === "FAIL")) {
|
|
71
|
-
return "FAIL";
|
|
72
|
-
}
|
|
73
|
-
if (gating.some((result) => result.status === "UNKNOWN")) {
|
|
74
|
-
return "UNKNOWN";
|
|
75
|
-
}
|
|
76
|
-
return "PASS";
|
|
77
|
-
}
|
|
78
23
|
function relevantCriteria(task, commandId) {
|
|
79
24
|
return task.criteria.filter((criterion) => criterion.verifierIds.includes(commandId));
|
|
80
25
|
}
|
|
@@ -85,9 +30,6 @@ function commandById(config, commandId) {
|
|
|
85
30
|
}
|
|
86
31
|
return command;
|
|
87
32
|
}
|
|
88
|
-
function diagnostics(spawned) {
|
|
89
|
-
return spawned.stderr || spawned.stdout || spawned.failureClass;
|
|
90
|
-
}
|
|
91
33
|
export class VerificationService {
|
|
92
34
|
#options;
|
|
93
35
|
constructor(options) {
|
|
@@ -120,51 +62,37 @@ export class VerificationService {
|
|
|
120
62
|
}
|
|
121
63
|
async #runCommand(task, command) {
|
|
122
64
|
const startedAt = (this.#options.now ?? (() => new Date().toISOString()))();
|
|
123
|
-
const
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
commandId: command.id,
|
|
130
|
-
status: "UNKNOWN",
|
|
131
|
-
failureClass: "repository-untrusted",
|
|
132
|
-
exitCode: null,
|
|
133
|
-
signal: null,
|
|
134
|
-
timedOut: false,
|
|
135
|
-
durationMs: 0,
|
|
136
|
-
stdout: "",
|
|
137
|
-
stderr: "",
|
|
138
|
-
stdoutTruncated: false,
|
|
139
|
-
stderrTruncated: false
|
|
140
|
-
};
|
|
141
|
-
const classified = classifyResult(command, spawned);
|
|
142
|
-
const fingerprint = classified.status === "PASS"
|
|
65
|
+
const result = await executeConfiguredCommand(command, {
|
|
66
|
+
cwd: await this.#commandCwd(command),
|
|
67
|
+
runner: this.#options.processRunner,
|
|
68
|
+
trusted: this.#options.trusted
|
|
69
|
+
});
|
|
70
|
+
const fingerprint = result.status === "PASS"
|
|
143
71
|
? null
|
|
144
72
|
: createFailureFingerprint({
|
|
145
73
|
commandId: command.id,
|
|
146
|
-
failureClass:
|
|
147
|
-
exitCategory:
|
|
74
|
+
failureClass: result.failureClass,
|
|
75
|
+
exitCategory: result.timedOut
|
|
148
76
|
? "timeout"
|
|
149
|
-
:
|
|
77
|
+
: result.signal !== null
|
|
150
78
|
? "signal-exit"
|
|
151
|
-
:
|
|
79
|
+
: result.exitCode === null
|
|
152
80
|
? "no-exit"
|
|
153
|
-
:
|
|
81
|
+
: result.exitCode === 0
|
|
154
82
|
? "exit-zero"
|
|
155
83
|
: "nonzero-exit",
|
|
156
|
-
diagnostics:
|
|
84
|
+
diagnostics: result.diagnostic
|
|
157
85
|
});
|
|
158
86
|
const finishedAt = (this.#options.now ?? (() => new Date().toISOString()))();
|
|
159
|
-
const evidenceReferences = await this.#persistEvidence(task, command, startedAt, finishedAt,
|
|
87
|
+
const evidenceReferences = await this.#persistEvidence(task, command, startedAt, finishedAt, result, result.exitCode);
|
|
160
88
|
return {
|
|
161
89
|
commandId: command.id,
|
|
162
90
|
required: command.required,
|
|
163
|
-
status:
|
|
164
|
-
failureClass:
|
|
165
|
-
exitCode:
|
|
166
|
-
timedOut:
|
|
167
|
-
testCount:
|
|
91
|
+
status: result.status,
|
|
92
|
+
failureClass: result.failureClass,
|
|
93
|
+
exitCode: result.exitCode,
|
|
94
|
+
timedOut: result.timedOut,
|
|
95
|
+
testCount: result.testCount,
|
|
168
96
|
diagnostic: fingerprint?.diagnostics ?? "",
|
|
169
97
|
evidenceReferences
|
|
170
98
|
};
|
|
@@ -185,7 +113,7 @@ export class VerificationService {
|
|
|
185
113
|
for (const commandId of selection.verifierIds) {
|
|
186
114
|
results.push(await this.#runCommand(validation.value, commandById(this.#options.config, commandId)));
|
|
187
115
|
}
|
|
188
|
-
const status =
|
|
116
|
+
const status = aggregateVerificationStatus(results);
|
|
189
117
|
let signal = null;
|
|
190
118
|
if (status === "PASS") {
|
|
191
119
|
await this.#options.taskService.clearFailure(taskId);
|
|
@@ -138,6 +138,10 @@ export class NodeVerificationProcessRunner {
|
|
|
138
138
|
const child = spawn(request.command, [...request.args], {
|
|
139
139
|
cwd: request.cwd,
|
|
140
140
|
detached: this.#platform !== "win32",
|
|
141
|
+
env: {
|
|
142
|
+
...process.env,
|
|
143
|
+
...(request.env ?? {})
|
|
144
|
+
},
|
|
141
145
|
shell: request.shell,
|
|
142
146
|
stdio: ["ignore", "pipe", "pipe"],
|
|
143
147
|
windowsHide: true
|
|
@@ -258,7 +262,8 @@ export async function runVerificationCommand(command, options) {
|
|
|
258
262
|
command: command.command,
|
|
259
263
|
args: [...command.args],
|
|
260
264
|
cwd: options.cwd,
|
|
261
|
-
shell: command.shell === true
|
|
265
|
+
shell: command.shell === true,
|
|
266
|
+
...(options.env === undefined ? {} : { env: options.env })
|
|
262
267
|
});
|
|
263
268
|
}
|
|
264
269
|
catch {
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
# Configuration
|
|
2
|
+
|
|
3
|
+
Keep project configuration explicit and layered. Choose scope, harness, and profile deliberately; do not infer trust or security exceptions from `--yes`.
|
|
4
|
+
|
|
5
|
+
Use `--harness all` to select Codex, Claude Code, and opencode, or pass a
|
|
6
|
+
comma-separated subset such as `codex,opencode`. `both` remains an input alias
|
|
7
|
+
for the legacy Codex plus Claude selection.
|
|
8
|
+
|
|
9
|
+
Project Codex and opencode installations share the managed supplemental
|
|
10
|
+
`AGENTS.md` routing block and the `.agent-ops/AGENTS.md` rules artifact. The
|
|
11
|
+
block loads the managed baseline while project-specific instructions remain
|
|
12
|
+
authoritative. Claude uses the corresponding `CLAUDE.md` route and
|
|
13
|
+
`.agent-ops/CLAUDE.md` artifact. Opencode additionally gets
|
|
14
|
+
the agent-ops-owned `.opencode/plugins/agent-ops.js` file; `opencode.json` is
|
|
15
|
+
never modified. The plugin is generated with the installed absolute runtime
|
|
16
|
+
path, so update it through `agent-ops update` rather than editing it manually.
|
|
17
|
+
|
|
18
|
+
At user scope, Codex and opencode keep separate routing files under `.codex/`
|
|
19
|
+
and `.opencode/`; the global opencode plugin is placed under
|
|
20
|
+
`.config/opencode/plugins/`, or under `$XDG_CONFIG_HOME/opencode/plugins/`
|
|
21
|
+
when that variable points inside the managed user root. If OpenCode is
|
|
22
|
+
configured with `$OPENCODE_CONFIG_DIR`, the plugin is placed under its
|
|
23
|
+
`plugins/` directory instead. The installer discovers writable harness
|
|
24
|
+
surfaces and applies the selected target policy; use
|
|
25
|
+
`--hook-target <harness>=<surface-id>` when the managed default is not the
|
|
26
|
+
intended surface. Project-local Claude hooks use `.claude/settings.json` by
|
|
27
|
+
default; select `.claude/settings.local.json` explicitly when that is the
|
|
28
|
+
intended surface.
|
|
29
|
+
Advisory and guardrail hooks are registered only when the selected profile
|
|
30
|
+
implies them. Advisory runs through the real SessionStart path and is
|
|
31
|
+
fail-open. Claude and Codex lifecycle support is `supported`; OpenCode begins
|
|
32
|
+
at app initialization and is honestly reported as `degraded`.
|
|
33
|
+
|
|
34
|
+
### Project-local loop profile
|
|
35
|
+
|
|
36
|
+
`--profile loop` is an opt-in project-scope profile. Select `codex`, `claude`,
|
|
37
|
+
or both (for example, `--harness codex,claude`); it requires a
|
|
38
|
+
POSIX-compatible `bash` and does not support Windows launchers yet. Start with
|
|
39
|
+
a dry run:
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
agent-ops init --dry-run --scope project --harness codex,claude --profile loop --json
|
|
43
|
+
agent-ops init --scope project --harness codex,claude --profile loop --yes
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
For each selected supported harness, agent-ops owns exactly one small launcher:
|
|
47
|
+
`.codex/hooks/agent-ops-loop.sh` or `.claude/hooks/agent-ops-loop.sh`. Both
|
|
48
|
+
launchers delegate to the same installed Node runtime, so they do not copy a
|
|
49
|
+
project-specific loop script. Codex also gets `.codex/config.toml` only when it
|
|
50
|
+
is absent. First installation seeds, without replacing existing content,
|
|
51
|
+
`loop-goal.md`, `loop-state.md`, and `loop-telemetry.jsonl` under the selected
|
|
52
|
+
harness directory. A hash-commented `.gitignore` block ignores those local
|
|
53
|
+
files.
|
|
54
|
+
|
|
55
|
+
The loop runs `SessionStart`, `UserPromptSubmit`, `PreToolUse`,
|
|
56
|
+
`PermissionRequest`, `PostToolUse`, `PreCompact`, `PostCompact`,
|
|
57
|
+
`SubagentStart`, and `SubagentStop`, but never adds `Stop`. It blocks only
|
|
58
|
+
high-confidence literal secrets in prompts or Bash commands, plus dangerous
|
|
59
|
+
Bash commands (including broad recursive deletion and `git reset --hard`). Codex uses its native
|
|
60
|
+
exit-code blocking mechanism; Claude Code receives its documented native JSON
|
|
61
|
+
decision shape. A `PermissionRequest`, including
|
|
62
|
+
`sandbox_permissions: "require_escalated"`, only records an outcome and emits
|
|
63
|
+
no allow or deny decision, preserving the host's normal approval flow.
|
|
64
|
+
|
|
65
|
+
Session context, telemetry, and compaction state are deliberately bounded.
|
|
66
|
+
Telemetry contains only timestamp, event, outcome, and rule identifier—not raw
|
|
67
|
+
prompts, commands, or credentials—and rotates by byte size. A pre-compaction
|
|
68
|
+
Git-status snapshot is redacted and written into a dedicated block in
|
|
69
|
+
`loop-state.md`, leaving surrounding user content intact. Installer update and
|
|
70
|
+
uninstall own only the launchers, native handler registrations, and exact
|
|
71
|
+
`.gitignore` block; goals, state, telemetry, and `config.toml` remain local
|
|
72
|
+
user files. If an existing `.codex/config.toml` explicitly says
|
|
73
|
+
`[features]` then `hooks = false`, planning stops with
|
|
74
|
+
`CODEX_LOOP_HOOKS_DISABLED` before any write.
|
|
75
|
+
|
|
76
|
+
Codex and Claude Code require their normal project-hook trust/review flow for
|
|
77
|
+
these generated handlers. The loop is a focused guardrail, not a complete
|
|
78
|
+
sandbox, permission bypass, or Stop-verification feature. See the [Codex hook
|
|
79
|
+
documentation](https://developers.openai.com/codex/config-advanced#hooks) and
|
|
80
|
+
the [Claude Code hook documentation](https://code.claude.com/docs/en/hooks)
|
|
81
|
+
before enabling it.
|
|
82
|
+
|
|
83
|
+
### Runtime-failure safeguards
|
|
84
|
+
|
|
85
|
+
For the ordinary `guardrails` profile, `command-policy` is the only capability with a fail-closed failure mode. Claude
|
|
86
|
+
Code can emit its documented denial shape at native `PreToolUse` for a
|
|
87
|
+
classified invalid installed configuration. The managed OpenCode
|
|
88
|
+
`tool.execute.before` plugin can throw its documented command-policy denial or
|
|
89
|
+
unavailable-runtime error for its supported Bash surface. Codex is explicitly
|
|
90
|
+
non-enforcing (`unknown`). These are agent-ops output and plugin contracts, not
|
|
91
|
+
proof that a host honors a denial. `SessionStart` and `Stop` failure paths stay
|
|
92
|
+
fail-open for every adapter.
|
|
93
|
+
|
|
94
|
+
Claude's invalid-config fallback has four safeguards: (1) an absent project
|
|
95
|
+
configuration stays fail-open, so only an invalid `.agent-ops/config.json` can
|
|
96
|
+
reach the fallback; (2) the manifest must safely prove that the current harness
|
|
97
|
+
is installed; (3) a human can export `AGENT_OPS_DISABLE=1` in the shell before
|
|
98
|
+
launching the host to restore fail-open temporarily; and (4) a Claude Code
|
|
99
|
+
denial names the config path and tells the user to repair it or temporarily set
|
|
100
|
+
that shell variable. The variable is read only from the hook-process environment
|
|
101
|
+
and cannot be set in agent-ops configuration, a manifest, or managed files.
|
|
102
|
+
|
|
103
|
+
`guardrails` installs command policy but does not enable Stop verification. Stop
|
|
104
|
+
is a separate config-v2 feature and must be explicitly enabled with at least
|
|
105
|
+
one confirmed command:
|
|
106
|
+
|
|
107
|
+
```json
|
|
108
|
+
{
|
|
109
|
+
"features": { "stopVerification": { "enabled": true } },
|
|
110
|
+
"verification": {
|
|
111
|
+
"commands": [
|
|
112
|
+
{
|
|
113
|
+
"id": "unit",
|
|
114
|
+
"command": "npm",
|
|
115
|
+
"args": ["test"],
|
|
116
|
+
"cwd": ".",
|
|
117
|
+
"required": true,
|
|
118
|
+
"evidence": { "kind": "test-count", "minimum": 1 }
|
|
119
|
+
}
|
|
120
|
+
]
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
Changing this feature changes native registration. Run:
|
|
126
|
+
|
|
127
|
+
```bash
|
|
128
|
+
agent-ops update
|
|
129
|
+
agent-ops trust grant
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
Without `update`, doctor can report `UPDATE_REQUIRED` for registration drift.
|
|
133
|
+
Separately, after a toolkit upgrade or effective profile or capability change
|
|
134
|
+
alters an intact path-independent managed rules artifact,
|
|
135
|
+
`artifact-staleness` reports `DEGRADED` with `UPDATE_REQUIRED`. `agent-ops
|
|
136
|
+
update` regenerates the artifact and clears that result; a missing or
|
|
137
|
+
hash-mismatched artifact remains an `artifacts` `FAIL`. Without the new trust
|
|
138
|
+
grant, trust-gated hooks remain stale. Stop is report-only: it continues the
|
|
139
|
+
harness for `PASS`, `FAIL`, or `UNKNOWN`, emits only bounded command ID, exit
|
|
140
|
+
code, test-count, config-hash, and timestamp evidence, and never completes a
|
|
141
|
+
task. Config v1 migrates deterministically to v2 with Stop disabled; old
|
|
142
|
+
binaries cannot read the migrated config, and the routing migration is
|
|
143
|
+
one-way after it is applied.
|
|
144
|
+
|
|
145
|
+
To narrow an existing installation, pass the desired list to `agent-ops update
|
|
146
|
+
--harness`; shared paths remain managed while removed harness-owned artifacts,
|
|
147
|
+
markers, and hooks are reconciled.
|
|
148
|
+
|
|
149
|
+
Installations using the previous canonical routing wording are migrated by
|
|
150
|
+
`agent-ops update`. If a managed block was edited, the command fails closed
|
|
151
|
+
until the change is reviewed.
|
|
152
|
+
|
|
153
|
+
Dry-run human and JSON plans omit raw harness settings content. They expose the
|
|
154
|
+
expected hash, content hash, and a safe summary while the internal apply plan
|
|
155
|
+
retains the complete merged settings. The manifest remains schema v2.
|
|
156
|
+
|
|
157
|
+
Use the [acceptance and evidence rules](../spec/acceptance-and-evidence.md)
|
|
158
|
+
when adding verification commands, and the [adapter rules](../spec/harness-adapters.md)
|
|
159
|
+
when configuring Codex, Claude Code, or opencode behavior.
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# Quickstart
|
|
2
|
+
|
|
3
|
+
1. Define 2–5 acceptance criteria.
|
|
4
|
+
2. Inspect the smallest relevant scope.
|
|
5
|
+
3. Run a failing test before implementation.
|
|
6
|
+
4. Implement the smallest change, then run typecheck and tests.
|
|
7
|
+
5. Record evidence and request independent review before handoff.
|
|
8
|
+
|
|
9
|
+
See the [normative loop](../spec/loop-engineering.md) and [review rules](../spec/review.md).
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
# Security
|
|
2
|
+
|
|
3
|
+
Treat commands, logs, configuration, and external input as untrusted. Redact credentials, block destructive commands by default, and keep local state out of tracked project paths.
|
|
4
|
+
|
|
5
|
+
Read the [guardrails](../spec/guardrails.md), [judgment rules](../spec/judgment.md), and [maintenance rules](../spec/maintenance.md) before changing policy or hook files.
|
package/docs/en/spec/README.md
CHANGED
|
@@ -2,6 +2,15 @@
|
|
|
2
2
|
|
|
3
3
|
This is the normative English specification for bounded, evidence-driven work.
|
|
4
4
|
|
|
5
|
+
The harness adapter rules cover Codex, Claude Code, and opencode. The opencode
|
|
6
|
+
integration is a generated local plugin; it does not manage `opencode.json`.
|
|
7
|
+
|
|
8
|
+
Configuration is versioned independently from the manifest. Config v1 migrates
|
|
9
|
+
to config v2 with Stop verification disabled; changing the capability requires
|
|
10
|
+
`agent-ops update` followed by `agent-ops trust grant`. Stop verification is
|
|
11
|
+
explicit, trusted, report-only, and never completes a task. Dry-run plans keep
|
|
12
|
+
foreign settings opaque, and the routing migration is one-way once applied.
|
|
13
|
+
|
|
5
14
|
- [Loop engineering](./loop-engineering.md)
|
|
6
15
|
- [Acceptance and evidence](./acceptance-and-evidence.md)
|
|
7
16
|
- [Judgment](./judgment.md)
|
|
@@ -1,12 +1,22 @@
|
|
|
1
1
|
# Harness Adapters
|
|
2
2
|
|
|
3
|
+
OpenCode plugin behavior in this document was checked against the [official
|
|
4
|
+
plugin documentation](https://opencode.ai/docs/plugins/) and [Bun shell
|
|
5
|
+
documentation](https://bun.sh/docs/runtime/shell) on 2026-07-31. Codex and
|
|
6
|
+
Claude Code loop-hook behavior was checked against their [Codex hook
|
|
7
|
+
documentation](https://developers.openai.com/codex/config-advanced#hooks) and
|
|
8
|
+
[Claude Code hook documentation](https://code.claude.com/docs/en/hooks) on
|
|
9
|
+
2026-08-03. Revalidate: when any vendor reference changes.
|
|
10
|
+
|
|
3
11
|
## HARNESS-ADAPTER-001
|
|
4
12
|
|
|
5
|
-
An adapter MUST preserve native harness semantics and MUST
|
|
13
|
+
An adapter MUST preserve native harness semantics and MUST declare each
|
|
14
|
+
capability as supported, degraded, unsupported, or unknown.
|
|
6
15
|
|
|
7
16
|
- Trigger: Mapping portable lifecycle or review behavior into a native harness.
|
|
8
17
|
- Action: Keep ownership narrow, retain user configuration, and document limitations.
|
|
9
|
-
- Evidence: Adapter tests cover existing configuration
|
|
18
|
+
- Evidence: Adapter tests cover existing configuration, support declarations,
|
|
19
|
+
and native failure outcomes.
|
|
10
20
|
- Positive: `A Codex blocking outcome remains UNKNOWN when native denial is unconfirmed.`
|
|
11
21
|
- Negative: `Assume Claude exit semantics apply to Codex.`
|
|
12
22
|
|
|
@@ -19,3 +29,95 @@ An adapter MUST be idempotent and MUST avoid deleting user-owned handlers.
|
|
|
19
29
|
- Evidence: Existing configuration fixtures remain intact after apply and uninstall.
|
|
20
30
|
- Positive: `Managed handler updates while unrelated handlers remain byte-for-byte present.`
|
|
21
31
|
- Negative: `Replace the complete settings file with toolkit defaults.`
|
|
32
|
+
|
|
33
|
+
## HARNESS-ADAPTER-003
|
|
34
|
+
|
|
35
|
+
A file-backed adapter MUST register only hooks implied by the active
|
|
36
|
+
capabilities and MUST track generated source as one whole-file artifact.
|
|
37
|
+
|
|
38
|
+
- Trigger: Installing or probing a harness whose extension point is a plugin file.
|
|
39
|
+
- Action: For opencode, manage `.opencode/plugins/agent-ops.js` in a project or `.config/opencode/plugins/agent-ops.js` at user scope (`$XDG_CONFIG_HOME/opencode/plugins/agent-ops.js` when configured inside the managed user root, or `$OPENCODE_CONFIG_DIR/plugins/agent-ops.js` when that native config directory is configured), leave `opencode.json` untouched, and deduplicate a project `AGENTS.md` contribution by path.
|
|
40
|
+
- Evidence: The manifest contains the plugin hash, the generated source contains only the selected hook registrations, and shared project markers occur once.
|
|
41
|
+
- Positive: `codex,opencode` produces one project AGENTS route and one hashed opencode plugin.
|
|
42
|
+
- Negative: `Add an opencode.json instructions entry or register a plugin for a core-only profile.`
|
|
43
|
+
|
|
44
|
+
## HARNESS-ADAPTER-004
|
|
45
|
+
|
|
46
|
+
The opencode shim MUST invoke the absolute runtime path from the selected
|
|
47
|
+
project directory, MUST fail open for
|
|
48
|
+
advisory events, and MUST throw its documented command-policy error when the
|
|
49
|
+
runtime is unavailable.
|
|
50
|
+
|
|
51
|
+
- Trigger: The generated plugin invokes `agent-ops` or receives an invalid runtime decision.
|
|
52
|
+
- Action: Keep normalization and native output encoding in the runtime adapter,
|
|
53
|
+
throw its documented policy reason for a deny decision, and run
|
|
54
|
+
lifecycle-summary through the shared advisory implementation. App-scoped
|
|
55
|
+
plugin initialization remains degraded for per-session lifecycle fidelity.
|
|
56
|
+
- Evidence: Shim import tests cover allow, deny, and missing-runtime behavior;
|
|
57
|
+
denial fixtures assert output shape only; doctor reports OpenCode lifecycle
|
|
58
|
+
support as `DEGRADED`.
|
|
59
|
+
- Positive: `When the runtime is unavailable, SessionStart stays fail-open and the generated plugin throws its documented command-policy error for a Bash pre-tool hook.`
|
|
60
|
+
- Negative: `Fall back to a PATH-resolved agent-ops executable, claim an OpenCode host honors a thrown denial, or claim app initialization is a per-session Stop-equivalent.`
|
|
61
|
+
|
|
62
|
+
## HARNESS-ADAPTER-005
|
|
63
|
+
|
|
64
|
+
Each descriptor MUST expose separate control and runtime adapters. The control
|
|
65
|
+
adapter owns installation planning, routing, ownership, probes, and the
|
|
66
|
+
in-memory capability registration matrix. The runtime adapter owns native input
|
|
67
|
+
decoding, normalized events, native output encoding, and runtime-failure output.
|
|
68
|
+
|
|
69
|
+
- Trigger: Adding a harness surface or a generic capability.
|
|
70
|
+
- Action: Add the capability-to-native registration to the owning harness,
|
|
71
|
+
including its support level and runtime-failure mode; do not add native
|
|
72
|
+
events to a universal union.
|
|
73
|
+
- Evidence: Every declared `supported` registration is exercised through the
|
|
74
|
+
real CLI hook process; denial-shape fixtures assert documented wire shapes,
|
|
75
|
+
not host runtime enforcement; unsupported Stop/lifecycle registrations are
|
|
76
|
+
not reported as enforcement success.
|
|
77
|
+
- Positive: `A fail-closed Claude command-policy runtime failure produces the documented PreToolUse denial shape through runHookCommand.`
|
|
78
|
+
- Negative: `Mark SessionStart supported while dispatchHookEvent has no advisory implementation.`
|
|
79
|
+
|
|
80
|
+
## HARNESS-ADAPTER-006
|
|
81
|
+
|
|
82
|
+
The project-local `loop` profile MUST be opt-in, project scoped, and use one
|
|
83
|
+
shared runtime behind minimal Codex and Claude Code launchers. It MUST NOT copy
|
|
84
|
+
policy into project-specific scripts or alter an ordinary permission request.
|
|
85
|
+
|
|
86
|
+
- Trigger: A project selects `loop` with Codex, Claude Code, or both.
|
|
87
|
+
- Action: Generate only the selected `.codex/hooks/agent-ops-loop.sh` and/or
|
|
88
|
+
`.claude/hooks/agent-ops-loop.sh` launchers, register the documented loop
|
|
89
|
+
lifecycle events except `Stop`, and preserve foreign hook groups. Block only
|
|
90
|
+
high-confidence literal credentials at `UserPromptSubmit` or Bash
|
|
91
|
+
`PreToolUse`, and dangerous Bash commands at `PreToolUse`, using the documented native denial shape. Emit no
|
|
92
|
+
decision for `PermissionRequest`, including escalated permissions.
|
|
93
|
+
- Evidence: Install-plan, loop-runtime, update, uninstall, and doctor tests
|
|
94
|
+
cover generated paths, Codex/Claude wire output, privacy bounds,
|
|
95
|
+
configuration conflict handling, state preservation, and registration drift.
|
|
96
|
+
- Positive: `A Claude PreToolUse dangerous Bash command receives a native deny while a PermissionRequest produces no allow or deny decision.`
|
|
97
|
+
- Negative: `Copy a project loop policy into both shell launchers, auto-approve sandbox escalation, or add a loop Stop handler.`
|
|
98
|
+
|
|
99
|
+
The current registration matrix is intentionally asymmetric:
|
|
100
|
+
|
|
101
|
+
| Capability | Codex | Claude Code | OpenCode |
|
|
102
|
+
| --- | --- | --- | --- |
|
|
103
|
+
| lifecycle-summary | supported | supported | degraded |
|
|
104
|
+
| command-policy | unknown | supported | supported |
|
|
105
|
+
| optional-stop-verify | unsupported | supported | degraded |
|
|
106
|
+
|
|
107
|
+
For runtime-failure handling, only `command-policy` is fail-closed. Claude
|
|
108
|
+
Code can emit its documented `PreToolUse` denial shape for a classified invalid
|
|
109
|
+
installed configuration; the managed OpenCode `tool.execute.before` plugin can
|
|
110
|
+
throw its documented denial or unavailable-runtime error for its supported Bash
|
|
111
|
+
surface. Codex remains `unknown` and never emits a denial. Fixture tests assert
|
|
112
|
+
these wire and plugin shapes only; they do not prove that a host honors a
|
|
113
|
+
denial. Every `SessionStart` and `Stop` failure path remains fail-open.
|
|
114
|
+
|
|
115
|
+
Stop verification is explicit, trusted, report-only, and disabled by default.
|
|
116
|
+
Every Stop result continues the native harness and may carry only bounded
|
|
117
|
+
command evidence; it is never task-completion evidence.
|
|
118
|
+
|
|
119
|
+
The `loop` profile is separate from the ordinary capability matrix above. It
|
|
120
|
+
stores only bounded local event metadata, returns bounded redacted session
|
|
121
|
+
context, and preserves local goal, state, telemetry, and Codex TOML files on
|
|
122
|
+
update or uninstall. A clearly parsed `[features]` / `hooks = false` in an
|
|
123
|
+
existing Codex configuration MUST reject loop planning before any write.
|
|
@@ -19,3 +19,14 @@ Volatile harness facts MUST live in adapter or research documentation and MUST i
|
|
|
19
19
|
- Evidence: The document has a source and `Revalidate:` condition.
|
|
20
20
|
- Positive: `Revalidate: when the vendor hook reference changes.`
|
|
21
21
|
- Negative: `Treat a transient CLI flag as a permanent core rule.`
|
|
22
|
+
|
|
23
|
+
## MAINTAIN-PLUGIN-001
|
|
24
|
+
|
|
25
|
+
Generated harness plugins MUST be treated as managed artifacts and MUST NOT be
|
|
26
|
+
hand-edited in place.
|
|
27
|
+
|
|
28
|
+
- Trigger: Updating the runtime, capabilities, or a vendor plugin contract.
|
|
29
|
+
- Action: Regenerate the opencode shim, verify its manifest hash, and revalidate the vendor behavior before release.
|
|
30
|
+
- Evidence: The artifact hash and shim import tests pass; release documentation records the revalidation condition.
|
|
31
|
+
- Positive: `agent-ops update` rewrites a changed plugin after ownership checks pass.
|
|
32
|
+
- Negative: `Patch the opencode plugin manually and retain the old manifest hash.`
|