@kylecheng3146/agent-ops 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +83 -0
- package/SECURITY.md +22 -0
- package/dist/packages/cli/src/args.js +322 -0
- package/dist/packages/cli/src/bin.js +290 -0
- package/dist/packages/cli/src/cli.js +80 -0
- package/dist/packages/cli/src/commands/config.js +8 -0
- package/dist/packages/cli/src/commands/doctor.js +32 -0
- package/dist/packages/cli/src/commands/index.js +16 -0
- package/dist/packages/cli/src/commands/init.js +65 -0
- package/dist/packages/cli/src/commands/review.js +71 -0
- package/dist/packages/cli/src/commands/task.js +141 -0
- package/dist/packages/cli/src/commands/trust.js +48 -0
- package/dist/packages/cli/src/commands/uninstall.js +58 -0
- package/dist/packages/cli/src/commands/update.js +58 -0
- package/dist/packages/cli/src/commands/verify.js +108 -0
- package/dist/packages/cli/src/output.js +53 -0
- package/dist/packages/cli/src/plan-output.js +28 -0
- package/dist/packages/cli/src/wizard.js +75 -0
- package/dist/runtime/src/adapters/claude/config.js +109 -0
- package/dist/runtime/src/adapters/claude/events.js +8 -0
- package/dist/runtime/src/adapters/claude/input.js +38 -0
- package/dist/runtime/src/adapters/claude/output.js +30 -0
- package/dist/runtime/src/adapters/codex/config.js +93 -0
- package/dist/runtime/src/adapters/codex/events.js +8 -0
- package/dist/runtime/src/adapters/codex/input.js +41 -0
- package/dist/runtime/src/adapters/codex/output.js +24 -0
- package/dist/runtime/src/config/explain.js +34 -0
- package/dist/runtime/src/config/load.js +25 -0
- package/dist/runtime/src/config/merge.js +170 -0
- package/dist/runtime/src/config/migrate.js +61 -0
- package/dist/runtime/src/contracts.js +1 -0
- package/dist/runtime/src/discovery/go.js +145 -0
- package/dist/runtime/src/discovery/index.js +40 -0
- package/dist/runtime/src/discovery/make.js +162 -0
- package/dist/runtime/src/discovery/node.js +175 -0
- package/dist/runtime/src/discovery/python.js +163 -0
- package/dist/runtime/src/discovery/rust.js +159 -0
- package/dist/runtime/src/discovery/types.js +1 -0
- package/dist/runtime/src/fs/hash.js +19 -0
- package/dist/runtime/src/fs/managed-block.js +90 -0
- package/dist/runtime/src/fs/manifest.js +24 -0
- package/dist/runtime/src/fs/mutation-worker.js +185 -0
- package/dist/runtime/src/fs/paths.js +96 -0
- package/dist/runtime/src/fs/transaction.js +498 -0
- package/dist/runtime/src/guardrails/destructive.js +207 -0
- package/dist/runtime/src/guardrails/evaluate.js +9 -0
- package/dist/runtime/src/guardrails/exceptions.js +49 -0
- package/dist/runtime/src/guardrails/secrets.js +97 -0
- package/dist/runtime/src/guardrails/types.js +9 -0
- package/dist/runtime/src/hooks/dispatch.js +78 -0
- package/dist/runtime/src/hooks/events.js +1 -0
- package/dist/runtime/src/hooks/hook-entry.js +19 -0
- package/dist/runtime/src/hooks/normalize.js +59 -0
- package/dist/runtime/src/hooks/output.js +12 -0
- package/dist/runtime/src/hooks/shell.js +138 -0
- package/dist/runtime/src/hooks/stop-verify.js +70 -0
- package/dist/runtime/src/install/apply.js +70 -0
- package/dist/runtime/src/install/doctor.js +196 -0
- package/dist/runtime/src/install/harness.js +87 -0
- package/dist/runtime/src/install/ownership.js +84 -0
- package/dist/runtime/src/install/plan.js +257 -0
- package/dist/runtime/src/install/profiles.js +28 -0
- package/dist/runtime/src/install/types.js +1 -0
- package/dist/runtime/src/install/uninstall.js +206 -0
- package/dist/runtime/src/install/update.js +123 -0
- package/dist/runtime/src/logging/local-log.js +158 -0
- package/dist/runtime/src/registry/npm.js +141 -0
- package/dist/runtime/src/review/claude-runner.js +4 -0
- package/dist/runtime/src/review/codex-runner.js +4 -0
- package/dist/runtime/src/review/packet.js +10 -0
- package/dist/runtime/src/review/result.js +24 -0
- package/dist/runtime/src/review/roles.js +3 -0
- package/dist/runtime/src/review/runner.js +45 -0
- package/dist/runtime/src/schema/validate.js +584 -0
- package/dist/runtime/src/security/permissions.js +654 -0
- package/dist/runtime/src/security/redact.js +41 -0
- package/dist/runtime/src/security/trust.js +209 -0
- package/dist/runtime/src/task/render.js +43 -0
- package/dist/runtime/src/task/service.js +235 -0
- package/dist/runtime/src/task/store.js +265 -0
- package/dist/runtime/src/verify/change-surface.js +86 -0
- package/dist/runtime/src/verify/evidence.js +89 -0
- package/dist/runtime/src/verify/fingerprint.js +67 -0
- package/dist/runtime/src/verify/scope.js +69 -0
- package/dist/runtime/src/verify/service.js +217 -0
- package/dist/runtime/src/verify/spawn.js +326 -0
- package/dist/runtime/src/verify/test-count.js +148 -0
- package/docs/en/spec/README.md +13 -0
- package/docs/en/spec/acceptance-and-evidence.md +21 -0
- package/docs/en/spec/delegation.md +21 -0
- package/docs/en/spec/guardrails.md +21 -0
- package/docs/en/spec/harness-adapters.md +21 -0
- package/docs/en/spec/judgment.md +21 -0
- package/docs/en/spec/loop-engineering.md +23 -0
- package/docs/en/spec/maintenance.md +21 -0
- package/docs/en/spec/review.md +21 -0
- package/docs/en/spec/troubleshooting.md +21 -0
- package/docs/zh-TW/spec/README.md +13 -0
- package/docs/zh-TW/spec/acceptance-and-evidence.md +23 -0
- package/docs/zh-TW/spec/delegation.md +23 -0
- package/docs/zh-TW/spec/guardrails.md +23 -0
- package/docs/zh-TW/spec/harness-adapters.md +23 -0
- package/docs/zh-TW/spec/judgment.md +23 -0
- package/docs/zh-TW/spec/loop-engineering.md +23 -0
- package/docs/zh-TW/spec/maintenance.md +23 -0
- package/docs/zh-TW/spec/review.md +23 -0
- package/docs/zh-TW/spec/troubleshooting.md +23 -0
- package/package.json +41 -0
- package/schemas/config.schema.json +231 -0
- package/schemas/evidence.schema.json +115 -0
- package/schemas/manifest.schema.json +116 -0
- package/schemas/task.schema.json +59 -0
- package/templates/common/AGENTS.block.md +3 -0
- package/templates/common/CLAUDE.block.md +3 -0
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { AgentOpsError } from "../../../../runtime/src/fs/paths.js";
|
|
2
|
+
import { renderTaskMarkdown, safeTaskText } from "../../../../runtime/src/task/render.js";
|
|
3
|
+
import { errorEnvelope, okEnvelope } from "../output.js";
|
|
4
|
+
function isRecord(value) {
|
|
5
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
6
|
+
}
|
|
7
|
+
function parseCriterion(source) {
|
|
8
|
+
let value;
|
|
9
|
+
try {
|
|
10
|
+
value = JSON.parse(source);
|
|
11
|
+
}
|
|
12
|
+
catch {
|
|
13
|
+
throw new AgentOpsError("TASK_CRITERION_INVALID", "Each --criterion value must be a JSON criterion object.");
|
|
14
|
+
}
|
|
15
|
+
if (!isRecord(value) ||
|
|
16
|
+
Object.keys(value).sort().join(",") !==
|
|
17
|
+
"description,id,verifierIds" ||
|
|
18
|
+
typeof value.id !== "string" ||
|
|
19
|
+
typeof value.description !== "string" ||
|
|
20
|
+
!Array.isArray(value.verifierIds) ||
|
|
21
|
+
value.verifierIds.some((id) => typeof id !== "string")) {
|
|
22
|
+
throw new AgentOpsError("TASK_CRITERION_INVALID", "Each criterion requires only id, description, and verifierIds.");
|
|
23
|
+
}
|
|
24
|
+
return {
|
|
25
|
+
id: value.id,
|
|
26
|
+
description: value.description,
|
|
27
|
+
verifierIds: [...value.verifierIds]
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
function parseEvidence(values) {
|
|
31
|
+
const evidence = {};
|
|
32
|
+
for (const value of values) {
|
|
33
|
+
const separator = value.indexOf("=");
|
|
34
|
+
if (separator <= 0 || separator === value.length - 1) {
|
|
35
|
+
throw new AgentOpsError("TASK_EVIDENCE_INVALID", "Each --evidence value must use criterion-id=reference.");
|
|
36
|
+
}
|
|
37
|
+
const criterionId = value.slice(0, separator);
|
|
38
|
+
const reference = value.slice(separator + 1);
|
|
39
|
+
const references = evidence[criterionId] ?? [];
|
|
40
|
+
references.push(reference);
|
|
41
|
+
evidence[criterionId] = references;
|
|
42
|
+
}
|
|
43
|
+
return evidence;
|
|
44
|
+
}
|
|
45
|
+
function requireTaskId(args) {
|
|
46
|
+
if (args.taskId === undefined) {
|
|
47
|
+
throw new AgentOpsError("TASK_ID_REQUIRED", "This task action requires --task <id>.");
|
|
48
|
+
}
|
|
49
|
+
return args.taskId;
|
|
50
|
+
}
|
|
51
|
+
function taskAction(args) {
|
|
52
|
+
if (args.command !== "task" ||
|
|
53
|
+
args.action === undefined ||
|
|
54
|
+
![
|
|
55
|
+
"archive",
|
|
56
|
+
"attach",
|
|
57
|
+
"complete",
|
|
58
|
+
"create",
|
|
59
|
+
"export",
|
|
60
|
+
"status"
|
|
61
|
+
].includes(args.action)) {
|
|
62
|
+
throw new AgentOpsError("TASK_ACTION_INVALID", "A supported task action is required.");
|
|
63
|
+
}
|
|
64
|
+
return args.action;
|
|
65
|
+
}
|
|
66
|
+
function taskEnvelope(action, code, message, record) {
|
|
67
|
+
return okEnvelope(code, {
|
|
68
|
+
action,
|
|
69
|
+
message,
|
|
70
|
+
record,
|
|
71
|
+
text: renderTaskMarkdown(record)
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
function renderTaskList(records) {
|
|
75
|
+
if (records.length === 0) {
|
|
76
|
+
return "No tasks.\n";
|
|
77
|
+
}
|
|
78
|
+
return `${records
|
|
79
|
+
.map((record) => `- ${record.task.id} [${record.status}] ${safeTaskText(record.task.title)}`)
|
|
80
|
+
.join("\n")}\n`;
|
|
81
|
+
}
|
|
82
|
+
export async function runTaskCommand(options) {
|
|
83
|
+
try {
|
|
84
|
+
const action = taskAction(options.args);
|
|
85
|
+
const sessionId = options.args.sessionId ?? options.sessionId;
|
|
86
|
+
if (action === "create") {
|
|
87
|
+
if (options.args.title === undefined) {
|
|
88
|
+
throw new AgentOpsError("TASK_TITLE_REQUIRED", "Task creation requires --title.");
|
|
89
|
+
}
|
|
90
|
+
const record = await options.service.create({
|
|
91
|
+
title: options.args.title,
|
|
92
|
+
criteria: (options.args.criteria ?? []).map(parseCriterion)
|
|
93
|
+
});
|
|
94
|
+
return taskEnvelope(action, "TASK_CREATED", `Created task ${record.task.id}.`, record);
|
|
95
|
+
}
|
|
96
|
+
if (action === "status") {
|
|
97
|
+
if (options.args.taskId === undefined && sessionId === undefined) {
|
|
98
|
+
const records = await options.service.list();
|
|
99
|
+
return okEnvelope("TASK_LISTED", {
|
|
100
|
+
action,
|
|
101
|
+
message: `Listed ${records.length} task(s).`,
|
|
102
|
+
records,
|
|
103
|
+
text: renderTaskList(records)
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
const record = await options.service.status(options.args.taskId === undefined
|
|
107
|
+
? { sessionId }
|
|
108
|
+
: { taskId: options.args.taskId });
|
|
109
|
+
return taskEnvelope(action, "TASK_STATUS", `Read task ${record.task.id}.`, record);
|
|
110
|
+
}
|
|
111
|
+
if (action === "attach") {
|
|
112
|
+
if (sessionId === undefined) {
|
|
113
|
+
throw new AgentOpsError("TASK_SESSION_REQUIRED", "Task attachment requires an injected session identity or --session.");
|
|
114
|
+
}
|
|
115
|
+
const record = await options.service.attach(sessionId, requireTaskId(options.args));
|
|
116
|
+
return taskEnvelope(action, "TASK_ATTACHED", `Attached session to ${record.task.id}.`, record);
|
|
117
|
+
}
|
|
118
|
+
if (action === "complete") {
|
|
119
|
+
const record = await options.service.complete(requireTaskId(options.args), parseEvidence(options.args.evidence ?? []));
|
|
120
|
+
return taskEnvelope(action, "TASK_COMPLETED", `Completed task ${record.task.id}.`, record);
|
|
121
|
+
}
|
|
122
|
+
if (action === "archive") {
|
|
123
|
+
const record = await options.service.archive(requireTaskId(options.args));
|
|
124
|
+
return taskEnvelope(action, "TASK_ARCHIVED", `Archived task ${record.task.id}.`, record);
|
|
125
|
+
}
|
|
126
|
+
const taskId = requireTaskId(options.args);
|
|
127
|
+
const record = await options.service.status({ taskId });
|
|
128
|
+
return okEnvelope("TASK_EXPORTED", {
|
|
129
|
+
action,
|
|
130
|
+
message: `Exported task ${record.task.id}.`,
|
|
131
|
+
record,
|
|
132
|
+
text: await options.service.export(taskId)
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
catch (error) {
|
|
136
|
+
if (error instanceof AgentOpsError) {
|
|
137
|
+
return errorEnvelope(error.code, error.message);
|
|
138
|
+
}
|
|
139
|
+
throw error;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { okEnvelope } from "../output.js";
|
|
2
|
+
function trustError(code, message, binding) {
|
|
3
|
+
return {
|
|
4
|
+
code,
|
|
5
|
+
status: "error",
|
|
6
|
+
data: { binding },
|
|
7
|
+
errors: [{ code, message }]
|
|
8
|
+
};
|
|
9
|
+
}
|
|
10
|
+
export async function runTrustCommand(options) {
|
|
11
|
+
const binding = await options.calculateBinding();
|
|
12
|
+
switch (options.action) {
|
|
13
|
+
case "status": {
|
|
14
|
+
const trust = await options.store.status(binding);
|
|
15
|
+
return okEnvelope("TRUST_STATUS", {
|
|
16
|
+
binding,
|
|
17
|
+
trust,
|
|
18
|
+
message: `Repository trust status: ${trust.status}`
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
case "revoke": {
|
|
22
|
+
const revoked = await options.store.revoke(binding);
|
|
23
|
+
return okEnvelope("TRUST_REVOKED", {
|
|
24
|
+
binding,
|
|
25
|
+
revoked,
|
|
26
|
+
message: revoked
|
|
27
|
+
? "Repository trust was revoked."
|
|
28
|
+
: "No exact repository trust record existed."
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
case "grant": {
|
|
32
|
+
await options.presentBinding(binding);
|
|
33
|
+
if (!options.isTTY && !options.yes) {
|
|
34
|
+
return trustError("TRUST_CONFIRMATION_REQUIRED", "Non-interactive trust grant requires: agent-ops trust grant --yes", binding);
|
|
35
|
+
}
|
|
36
|
+
if (options.isTTY &&
|
|
37
|
+
!options.yes &&
|
|
38
|
+
!(await options.confirmGrant(binding))) {
|
|
39
|
+
return trustError("TRUST_GRANT_CANCELLED", "Repository trust was not granted.", binding);
|
|
40
|
+
}
|
|
41
|
+
await options.store.grant(binding);
|
|
42
|
+
return okEnvelope("TRUST_GRANTED", {
|
|
43
|
+
binding,
|
|
44
|
+
message: "Repository trust was granted for the displayed binding."
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { applyUninstallPlan, createUninstallPlan } from "../../../../runtime/src/install/uninstall.js";
|
|
2
|
+
import { okEnvelope } from "../output.js";
|
|
3
|
+
import { formatOperationPlan } from "../plan-output.js";
|
|
4
|
+
function formatUninstallPlan(plan) {
|
|
5
|
+
return formatOperationPlan({
|
|
6
|
+
title: "Uninstall plan",
|
|
7
|
+
metadata: [
|
|
8
|
+
`Installed: ${plan.installed ? "yes" : "no"}`,
|
|
9
|
+
...(plan.manifest === null
|
|
10
|
+
? []
|
|
11
|
+
: [
|
|
12
|
+
`Scope: ${plan.manifest.scope}`,
|
|
13
|
+
`Harness: ${plan.manifest.harness}`
|
|
14
|
+
])
|
|
15
|
+
],
|
|
16
|
+
operations: plan.operations
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
function uninstallError(code, message, plan) {
|
|
20
|
+
return {
|
|
21
|
+
code,
|
|
22
|
+
status: "error",
|
|
23
|
+
data: { applied: false, plan, message },
|
|
24
|
+
errors: [{ code, message }]
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
export async function runUninstallCommand(options) {
|
|
28
|
+
const plan = await createUninstallPlan(options.root);
|
|
29
|
+
if (!plan.installed) {
|
|
30
|
+
return okEnvelope("UNINSTALL_NOT_INSTALLED", {
|
|
31
|
+
applied: false,
|
|
32
|
+
plan,
|
|
33
|
+
message: "No managed installation exists."
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
if (options.args.dryRun) {
|
|
37
|
+
return okEnvelope("UNINSTALL_PLAN_READY", {
|
|
38
|
+
applied: false,
|
|
39
|
+
plan,
|
|
40
|
+
message: "Uninstall plan calculated; no files were changed.",
|
|
41
|
+
text: formatUninstallPlan(plan)
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
if (!options.args.yes && !options.isTTY) {
|
|
45
|
+
return uninstallError("UNINSTALL_CONFIRMATION_REQUIRED", "Non-interactive uninstall requires --yes.", plan);
|
|
46
|
+
}
|
|
47
|
+
if (!options.args.yes &&
|
|
48
|
+
!(await options.confirm(plan))) {
|
|
49
|
+
return uninstallError("UNINSTALL_CANCELLED", "Uninstall was cancelled; no files were changed.", plan);
|
|
50
|
+
}
|
|
51
|
+
await applyUninstallPlan(options.root, plan);
|
|
52
|
+
return okEnvelope("UNINSTALL_APPLIED", {
|
|
53
|
+
applied: true,
|
|
54
|
+
plan,
|
|
55
|
+
message: "Managed installation content was removed."
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
export { formatUninstallPlan };
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { applyUpdatePlan, createUpdatePlan } from "../../../../runtime/src/install/update.js";
|
|
2
|
+
import { okEnvelope } from "../output.js";
|
|
3
|
+
import { formatOperationPlan } from "../plan-output.js";
|
|
4
|
+
export function formatUpdatePlan(plan) {
|
|
5
|
+
return formatOperationPlan({
|
|
6
|
+
title: "Update plan",
|
|
7
|
+
metadata: [
|
|
8
|
+
`Target version: ${plan.targetVersion}`,
|
|
9
|
+
`Schema migrations: ${plan.migrationSteps.length === 0
|
|
10
|
+
? "none"
|
|
11
|
+
: plan.migrationSteps
|
|
12
|
+
.map(({ fromVersion, toVersion }) => `${fromVersion} -> ${toVersion}`)
|
|
13
|
+
.join(", ")}`
|
|
14
|
+
],
|
|
15
|
+
operations: plan.installation.operations
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
function updateError(code, message, plan) {
|
|
19
|
+
return {
|
|
20
|
+
code,
|
|
21
|
+
status: "error",
|
|
22
|
+
data: { applied: false, plan, message },
|
|
23
|
+
errors: [{ code, message }]
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
export async function runUpdateCommand(options) {
|
|
27
|
+
const plan = await createUpdatePlan({
|
|
28
|
+
root: options.root,
|
|
29
|
+
adapters: options.adapters,
|
|
30
|
+
...(options.registry === undefined
|
|
31
|
+
? {}
|
|
32
|
+
: { registry: options.registry }),
|
|
33
|
+
...(options.targetVersion === undefined
|
|
34
|
+
? {}
|
|
35
|
+
: { targetVersion: options.targetVersion })
|
|
36
|
+
});
|
|
37
|
+
if (options.args.dryRun) {
|
|
38
|
+
return okEnvelope("UPDATE_PLAN_READY", {
|
|
39
|
+
applied: false,
|
|
40
|
+
plan,
|
|
41
|
+
message: "Update plan calculated; no files were changed.",
|
|
42
|
+
text: formatUpdatePlan(plan)
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
if (!options.args.yes && !options.isTTY) {
|
|
46
|
+
return updateError("UPDATE_CONFIRMATION_REQUIRED", "Non-interactive update requires --yes.", plan);
|
|
47
|
+
}
|
|
48
|
+
if (!options.args.yes &&
|
|
49
|
+
!(await options.confirm(plan))) {
|
|
50
|
+
return updateError("UPDATE_CANCELLED", "Update was cancelled; no files were changed.", plan);
|
|
51
|
+
}
|
|
52
|
+
await applyUpdatePlan(options.root, plan);
|
|
53
|
+
return okEnvelope("UPDATE_APPLIED", {
|
|
54
|
+
applied: true,
|
|
55
|
+
plan,
|
|
56
|
+
message: `Managed installation updated to ${plan.targetVersion}.`
|
|
57
|
+
});
|
|
58
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { AgentOpsError } from "../../../../runtime/src/fs/paths.js";
|
|
2
|
+
import { redactSecrets } from "../../../../runtime/src/security/redact.js";
|
|
3
|
+
import { safeTaskText } from "../../../../runtime/src/task/render.js";
|
|
4
|
+
import { errorEnvelope, okEnvelope } from "../output.js";
|
|
5
|
+
function safe(value) {
|
|
6
|
+
return safeTaskText(redactSecrets(value));
|
|
7
|
+
}
|
|
8
|
+
function publicReport(report) {
|
|
9
|
+
return {
|
|
10
|
+
taskId: report.taskId,
|
|
11
|
+
status: report.status,
|
|
12
|
+
surface: {
|
|
13
|
+
staged: report.surface.staged.map(redactSecrets),
|
|
14
|
+
unstaged: report.surface.unstaged.map(redactSecrets),
|
|
15
|
+
untracked: report.surface.untracked.map(redactSecrets),
|
|
16
|
+
paths: report.surface.paths.map(redactSecrets)
|
|
17
|
+
},
|
|
18
|
+
selection: {
|
|
19
|
+
verifierIds: [...report.selection.verifierIds],
|
|
20
|
+
fallback: report.selection.fallback,
|
|
21
|
+
reason: report.selection.reason,
|
|
22
|
+
evidence: {
|
|
23
|
+
changedPaths: report.selection.evidence.changedPaths.map(redactSecrets),
|
|
24
|
+
mappings: report.selection.evidence.mappings.map((mapping) => ({
|
|
25
|
+
changedPath: redactSecrets(mapping.changedPath),
|
|
26
|
+
mappingPaths: mapping.mappingPaths.map(redactSecrets),
|
|
27
|
+
verifierIds: [...mapping.verifierIds]
|
|
28
|
+
})),
|
|
29
|
+
requiredVerifierIds: [
|
|
30
|
+
...report.selection.evidence.requiredVerifierIds
|
|
31
|
+
]
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
results: report.results.map((result) => ({
|
|
35
|
+
commandId: result.commandId,
|
|
36
|
+
required: result.required,
|
|
37
|
+
status: result.status,
|
|
38
|
+
failureClass: result.failureClass,
|
|
39
|
+
exitCode: result.exitCode,
|
|
40
|
+
timedOut: result.timedOut,
|
|
41
|
+
testCount: result.testCount,
|
|
42
|
+
evidenceReferences: result.evidenceReferences.map(redactSecrets)
|
|
43
|
+
})),
|
|
44
|
+
signal: report.signal
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
function formatResult(result) {
|
|
48
|
+
const testCount = result.testCount === null ? "" : ` (tests: ${result.testCount})`;
|
|
49
|
+
const lines = [
|
|
50
|
+
`- ${result.status} ${safe(result.commandId)}${testCount}`
|
|
51
|
+
];
|
|
52
|
+
if (result.evidenceReferences.length > 0) {
|
|
53
|
+
lines.push(...result.evidenceReferences.map((reference) => ` Evidence: ${safe(reference)}`));
|
|
54
|
+
}
|
|
55
|
+
return lines;
|
|
56
|
+
}
|
|
57
|
+
function formatReport(report) {
|
|
58
|
+
const lines = [
|
|
59
|
+
`Verification task ${safe(report.taskId)}: ${report.status}`,
|
|
60
|
+
`Scope: ${report.selection.reason} (${report.selection.fallback ? "fallback" : "bounded"})`,
|
|
61
|
+
...report.results.flatMap(formatResult)
|
|
62
|
+
];
|
|
63
|
+
if (report.signal !== null) {
|
|
64
|
+
lines.push(`Signal: ${report.signal}`);
|
|
65
|
+
}
|
|
66
|
+
return `${lines.join("\n")}\n`;
|
|
67
|
+
}
|
|
68
|
+
async function resolveTaskId(options) {
|
|
69
|
+
if (options.args.taskId !== undefined) {
|
|
70
|
+
return options.args.taskId;
|
|
71
|
+
}
|
|
72
|
+
const sessionId = options.args.sessionId ?? options.sessionId;
|
|
73
|
+
if (sessionId === undefined) {
|
|
74
|
+
throw new AgentOpsError("VERIFY_TASK_REQUIRED", "Verification requires --task, --session, or an injected session identity.");
|
|
75
|
+
}
|
|
76
|
+
return (await options.taskService.status({ sessionId })).task.id;
|
|
77
|
+
}
|
|
78
|
+
export async function runVerifyCommand(options) {
|
|
79
|
+
try {
|
|
80
|
+
const report = publicReport(await options.service.verify(await resolveTaskId(options)));
|
|
81
|
+
const text = formatReport(report);
|
|
82
|
+
if (report.status === "PASS") {
|
|
83
|
+
return okEnvelope("VERIFICATION_PASSED", {
|
|
84
|
+
message: "Required verification passed.",
|
|
85
|
+
report,
|
|
86
|
+
text
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
const code = report.status === "FAIL"
|
|
90
|
+
? "VERIFICATION_FAILED"
|
|
91
|
+
: "VERIFICATION_UNKNOWN";
|
|
92
|
+
const message = report.status === "FAIL"
|
|
93
|
+
? "Required verification failed."
|
|
94
|
+
: "Required verification could not be established.";
|
|
95
|
+
return {
|
|
96
|
+
code,
|
|
97
|
+
status: "error",
|
|
98
|
+
data: { message, report, text },
|
|
99
|
+
errors: [{ code, message }]
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
catch (error) {
|
|
103
|
+
if (error instanceof AgentOpsError) {
|
|
104
|
+
return errorEnvelope(error.code, safe(error.message));
|
|
105
|
+
}
|
|
106
|
+
throw error;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
export function okEnvelope(code, data) {
|
|
2
|
+
return {
|
|
3
|
+
code,
|
|
4
|
+
status: "ok",
|
|
5
|
+
data: data === undefined ? null : data,
|
|
6
|
+
errors: []
|
|
7
|
+
};
|
|
8
|
+
}
|
|
9
|
+
export function errorEnvelope(code, message) {
|
|
10
|
+
return {
|
|
11
|
+
code,
|
|
12
|
+
status: "error",
|
|
13
|
+
data: null,
|
|
14
|
+
errors: [{ code, message }]
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
function humanSuccess(envelope) {
|
|
18
|
+
if (typeof envelope.data === "object" && envelope.data !== null) {
|
|
19
|
+
const data = envelope.data;
|
|
20
|
+
if (typeof data.text === "string") {
|
|
21
|
+
return data.text.endsWith("\n") ? data.text : `${data.text}\n`;
|
|
22
|
+
}
|
|
23
|
+
if (typeof data.version === "string") {
|
|
24
|
+
return `${data.version}\n`;
|
|
25
|
+
}
|
|
26
|
+
if (typeof data.message === "string") {
|
|
27
|
+
return `${data.message}\n`;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return `${envelope.code}\n`;
|
|
31
|
+
}
|
|
32
|
+
function humanError(envelope) {
|
|
33
|
+
if (typeof envelope.data === "object" && envelope.data !== null) {
|
|
34
|
+
const data = envelope.data;
|
|
35
|
+
if (typeof data.text === "string") {
|
|
36
|
+
return data.text.endsWith("\n") ? data.text : `${data.text}\n`;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
const message = envelope.errors[0]?.message ?? envelope.code;
|
|
40
|
+
return `${message}\n`;
|
|
41
|
+
}
|
|
42
|
+
export function writeEnvelope(sink, envelope, json) {
|
|
43
|
+
if (json) {
|
|
44
|
+
const normalized = envelope.data === undefined ? { ...envelope, data: null } : envelope;
|
|
45
|
+
sink.writeStdout(`${JSON.stringify(normalized)}\n`);
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
if (envelope.status === "ok") {
|
|
49
|
+
sink.writeStdout(humanSuccess(envelope));
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
sink.writeStderr(humanError(envelope));
|
|
53
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
function escapeTerminalText(value) {
|
|
2
|
+
return value.replace(/[\u0000-\u0009\u000B-\u001F\u007F-\u009F\u202A-\u202E\u2066-\u2069]/gu, (character) => {
|
|
3
|
+
const codePoint = character.codePointAt(0);
|
|
4
|
+
if (codePoint === undefined) {
|
|
5
|
+
return "\\u{fffd}";
|
|
6
|
+
}
|
|
7
|
+
return codePoint <= 0xff
|
|
8
|
+
? `\\x${codePoint.toString(16).padStart(2, "0")}`
|
|
9
|
+
: `\\u{${codePoint.toString(16)}}`;
|
|
10
|
+
});
|
|
11
|
+
}
|
|
12
|
+
export function formatOperationPlan(options) {
|
|
13
|
+
const lines = [
|
|
14
|
+
options.title,
|
|
15
|
+
...options.metadata.map(escapeTerminalText),
|
|
16
|
+
"Operations:"
|
|
17
|
+
];
|
|
18
|
+
for (const operation of options.operations) {
|
|
19
|
+
lines.push(`- ${operation.kind} ${escapeTerminalText(operation.path)}`, ` expected: ${operation.expectedHash ?? "<absent>"}`);
|
|
20
|
+
if (operation.kind === "write") {
|
|
21
|
+
lines.push(" content:", ...operation.content
|
|
22
|
+
.replace(/\n$/u, "")
|
|
23
|
+
.split("\n")
|
|
24
|
+
.map((line) => ` ${escapeTerminalText(line)}`));
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return `${lines.join("\n")}\n`;
|
|
28
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { CliArgumentError } from "./args.js";
|
|
2
|
+
const SCOPES = new Set(["project", "user"]);
|
|
3
|
+
const HARNESSES = new Set(["both", "claude", "codex"]);
|
|
4
|
+
const PROFILES = new Set(["advisory", "core", "guardrails"]);
|
|
5
|
+
async function createPromptSession(io) {
|
|
6
|
+
if (!io.isTTY) {
|
|
7
|
+
throw new CliArgumentError("CLI_INTERACTIVE_REQUIRED", "Missing init choices require an interactive terminal.");
|
|
8
|
+
}
|
|
9
|
+
if (io.prompt !== undefined) {
|
|
10
|
+
return {
|
|
11
|
+
question: io.prompt,
|
|
12
|
+
close: () => undefined
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
if (io.input === undefined || io.output === undefined) {
|
|
16
|
+
throw new CliArgumentError("CLI_INTERACTIVE_IO_REQUIRED", "Interactive input and output streams are unavailable.");
|
|
17
|
+
}
|
|
18
|
+
const { createInterface } = await import("node:readline/promises");
|
|
19
|
+
const readline = createInterface({
|
|
20
|
+
input: io.input,
|
|
21
|
+
output: io.output
|
|
22
|
+
});
|
|
23
|
+
return {
|
|
24
|
+
question: (prompt) => readline.question(prompt),
|
|
25
|
+
close: () => readline.close()
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
function selectValue(raw, fallback, allowed, label) {
|
|
29
|
+
const value = raw.trim() === "" ? fallback : raw.trim();
|
|
30
|
+
if (!allowed.has(value)) {
|
|
31
|
+
throw new CliArgumentError("CLI_INVALID_VALUE", `Invalid ${label}: ${value}`);
|
|
32
|
+
}
|
|
33
|
+
return value;
|
|
34
|
+
}
|
|
35
|
+
function selectProfiles(raw) {
|
|
36
|
+
const values = raw.trim() === ""
|
|
37
|
+
? ["core"]
|
|
38
|
+
: raw.split(",").map((value) => value.trim());
|
|
39
|
+
if (values.length === 0 ||
|
|
40
|
+
values.some((value) => !PROFILES.has(value)) ||
|
|
41
|
+
new Set(values).size !== values.length) {
|
|
42
|
+
throw new CliArgumentError("CLI_INVALID_VALUE", "Profiles must be a unique comma-separated list of core, advisory, or guardrails.");
|
|
43
|
+
}
|
|
44
|
+
return values;
|
|
45
|
+
}
|
|
46
|
+
export async function completeInitChoices(args, io) {
|
|
47
|
+
if (args.command !== "init" ||
|
|
48
|
+
(args.scope !== undefined &&
|
|
49
|
+
args.harness !== undefined &&
|
|
50
|
+
args.profiles.length > 0)) {
|
|
51
|
+
return args;
|
|
52
|
+
}
|
|
53
|
+
if (!io.isTTY) {
|
|
54
|
+
throw new CliArgumentError("CLI_INTERACTIVE_REQUIRED", "Non-interactive init requires --scope, --harness, and at least one --profile.");
|
|
55
|
+
}
|
|
56
|
+
const session = await createPromptSession(io);
|
|
57
|
+
try {
|
|
58
|
+
const scope = args.scope ??
|
|
59
|
+
selectValue(await session.question("Scope (project/user) [project]: "), "project", SCOPES, "scope");
|
|
60
|
+
const harness = args.harness ??
|
|
61
|
+
selectValue(await session.question("Harness (both/claude/codex) [both]: "), "both", HARNESSES, "harness");
|
|
62
|
+
const profiles = args.profiles.length > 0
|
|
63
|
+
? args.profiles
|
|
64
|
+
: selectProfiles(await session.question("Profiles (core,advisory,guardrails) [core]: "));
|
|
65
|
+
return {
|
|
66
|
+
...args,
|
|
67
|
+
scope,
|
|
68
|
+
harness,
|
|
69
|
+
profiles
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
finally {
|
|
73
|
+
session.close();
|
|
74
|
+
}
|
|
75
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { AgentOpsError } from "../../fs/paths.js";
|
|
2
|
+
export function claudeSettingsTarget(scope) {
|
|
3
|
+
return scope === "project"
|
|
4
|
+
? {
|
|
5
|
+
path: ".claude/settings.json",
|
|
6
|
+
requiresWorkspaceTrust: true
|
|
7
|
+
}
|
|
8
|
+
: {
|
|
9
|
+
path: "~/.claude/settings.json",
|
|
10
|
+
requiresWorkspaceTrust: false
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
export function claudeRoutingBlock() {
|
|
14
|
+
return [
|
|
15
|
+
"## Loop Engineering",
|
|
16
|
+
"",
|
|
17
|
+
"Load `.agent-ops/CLAUDE.md` as concise project context.",
|
|
18
|
+
""
|
|
19
|
+
].join("\n");
|
|
20
|
+
}
|
|
21
|
+
function commandHook(event, runtimePath) {
|
|
22
|
+
return {
|
|
23
|
+
type: "command",
|
|
24
|
+
command: "node",
|
|
25
|
+
args: [
|
|
26
|
+
runtimePath,
|
|
27
|
+
"claude",
|
|
28
|
+
event,
|
|
29
|
+
"--managed-by=agent-ops"
|
|
30
|
+
],
|
|
31
|
+
timeout: 30
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
function matcherGroup(event, runtimePath) {
|
|
35
|
+
return {
|
|
36
|
+
...(event === "PreToolUse" ? { matcher: "Bash" } : {}),
|
|
37
|
+
hooks: [commandHook(event, runtimePath)]
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
export function buildClaudeHookSettings(capabilities, runtimePath) {
|
|
41
|
+
if (runtimePath.length === 0 ||
|
|
42
|
+
runtimePath.length > 4096 ||
|
|
43
|
+
runtimePath.includes("\0")) {
|
|
44
|
+
throw new AgentOpsError("CLAUDE_HOOK_PATH_INVALID", "Claude hook runtime path is invalid.");
|
|
45
|
+
}
|
|
46
|
+
const hooks = {};
|
|
47
|
+
if (capabilities.includes("lifecycle-summary")) {
|
|
48
|
+
hooks.SessionStart = [matcherGroup("SessionStart", runtimePath)];
|
|
49
|
+
}
|
|
50
|
+
if (capabilities.includes("command-policy")) {
|
|
51
|
+
hooks.PreToolUse = [matcherGroup("PreToolUse", runtimePath)];
|
|
52
|
+
}
|
|
53
|
+
if (capabilities.includes("optional-stop-verify")) {
|
|
54
|
+
hooks.Stop = [matcherGroup("Stop", runtimePath)];
|
|
55
|
+
}
|
|
56
|
+
return { hooks };
|
|
57
|
+
}
|
|
58
|
+
function isRecord(value) {
|
|
59
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
60
|
+
}
|
|
61
|
+
function isOwnedHandler(handler) {
|
|
62
|
+
if (!isRecord(handler) ||
|
|
63
|
+
handler.command !== "node" ||
|
|
64
|
+
!Array.isArray(handler.args)) {
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
return handler.args[3] === "--managed-by=agent-ops";
|
|
68
|
+
}
|
|
69
|
+
function withoutOwnedHandlers(value) {
|
|
70
|
+
if (!isRecord(value) || !Array.isArray(value.hooks)) {
|
|
71
|
+
return value;
|
|
72
|
+
}
|
|
73
|
+
const hooks = value.hooks.filter((handler) => !isOwnedHandler(handler));
|
|
74
|
+
return hooks.length === 0 ? null : { ...value, hooks };
|
|
75
|
+
}
|
|
76
|
+
function hookRecord(settings) {
|
|
77
|
+
if (settings.hooks === undefined) {
|
|
78
|
+
return {};
|
|
79
|
+
}
|
|
80
|
+
if (!isRecord(settings.hooks) ||
|
|
81
|
+
Object.values(settings.hooks).some((groups) => !Array.isArray(groups))) {
|
|
82
|
+
throw new AgentOpsError("CLAUDE_SETTINGS_INVALID", "Claude hook groups must be arrays.");
|
|
83
|
+
}
|
|
84
|
+
return settings.hooks;
|
|
85
|
+
}
|
|
86
|
+
export function mergeClaudeSettings(existing, managed) {
|
|
87
|
+
if (!isRecord(existing)) {
|
|
88
|
+
throw new AgentOpsError("CLAUDE_SETTINGS_INVALID", "Claude settings must be a JSON object.");
|
|
89
|
+
}
|
|
90
|
+
const existingHooks = hookRecord(existing);
|
|
91
|
+
const hooks = {};
|
|
92
|
+
const eventNames = new Set([
|
|
93
|
+
...Object.keys(existingHooks),
|
|
94
|
+
...Object.keys(managed.hooks)
|
|
95
|
+
]);
|
|
96
|
+
for (const eventName of eventNames) {
|
|
97
|
+
const preserved = (existingHooks[eventName] ?? [])
|
|
98
|
+
.map(withoutOwnedHandlers)
|
|
99
|
+
.filter((group) => group !== null);
|
|
100
|
+
const additions = managed.hooks[eventName] ?? [];
|
|
101
|
+
if (preserved.length > 0 || additions.length > 0) {
|
|
102
|
+
hooks[eventName] = [...preserved, ...additions];
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return {
|
|
106
|
+
...existing,
|
|
107
|
+
hooks
|
|
108
|
+
};
|
|
109
|
+
}
|