@cassiomc1/forgeloop 1.6.2 → 1.6.4
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/DOCS_INDEX.md +26 -4
- package/EXECUTION_STATE.md +7 -0
- package/LOOP_ENGINEERING.md +58 -0
- package/LOOP_SYSTEM_DESIGN.md +54 -0
- package/ORCHESTRATOR_INTEGRATION.md +23 -0
- package/PROTOCOL_INTEGRATION.md +36 -0
- package/QUALITY_SCORECARD.md +21 -0
- package/README.md +57 -2
- package/TERMINOLOGY.md +11 -0
- package/THREAT_MODEL.md +7 -0
- package/completions/_forgeloop +85 -0
- package/completions/forgeloop.bash +173 -0
- package/completions/forgeloop.fish +544 -0
- package/docs/AGENT_PROTOCOL_SUMMARY.md +281 -0
- package/docs/ARTIFACT_REFERENCE.md +226 -0
- package/docs/CLI_REFERENCE.md +308 -2
- package/docs/CODE_ATTESTATION.md +141 -0
- package/docs/CROSS_HARNESS_CONTINUITY.md +22 -2
- package/docs/DOCUMENTATION_GUIDE.md +13 -11
- package/docs/GETTING_STARTED.md +98 -1
- package/docs/MCP.md +8 -0
- package/docs/PLATFORM_ADAPTERS.md +69 -0
- package/docs/RECIPES.md +138 -0
- package/docs/RELEASE_CHECKLIST.md +54 -0
- package/docs/REVISION_PROVIDERS.md +136 -0
- package/docs/SIGNING_PROVIDERS.md +83 -0
- package/docs/TROUBLESHOOTING.md +184 -0
- package/docs/UNIVERSAL_INTEGRATION.md +11 -0
- package/docs/assets/diagrams/forgeloop-code-attestation-flow.html +13841 -0
- package/docs/assets/diagrams/forgeloop-code-attestation-flow.receipt.json +37 -0
- package/docs/assets/diagrams/forgeloop-code-attestation-flow.svg +5046 -0
- package/docs/assets/diagrams/forgeloop-engineering-flow.html +46 -43
- package/docs/assets/diagrams/forgeloop-engineering-flow.receipt.json +6 -6
- package/docs/assets/diagrams/forgeloop-engineering-flow.svg +41 -41
- package/docs/assets/diagrams/forgeloop-verification-trust-flow.html +13783 -0
- package/docs/assets/diagrams/forgeloop-verification-trust-flow.receipt.json +37 -0
- package/docs/assets/diagrams/forgeloop-verification-trust-flow.svg +4988 -0
- package/docs/diagrams/README.md +30 -22
- package/docs/diagrams/forgeloop-code-attestation-flow.workflow.json +125 -0
- package/docs/diagrams/forgeloop-engineering-flow.workflow.json +11 -8
- package/docs/diagrams/forgeloop-verification-trust-flow.workflow.json +116 -0
- package/docs/diagrams/manifest.json +42 -0
- package/docs/diagrams/reviews/forgeloop-code-attestation-flow.review.json +20 -0
- package/docs/diagrams/reviews/forgeloop-engineering-flow.review.json +3 -3
- package/docs/diagrams/reviews/forgeloop-verification-trust-flow.review.json +20 -0
- package/integrations/generic-ci/verify.sh +14 -0
- package/package.json +31 -9
- package/schemas/attestation-verification-result.schema.json +34 -0
- package/schemas/code-attestation.schema.json +61 -0
- package/schemas/code-manifest.schema.json +57 -0
- package/schemas/config.schema.json +57 -1
- package/schemas/handoff-envelope.schema.json +57 -0
- package/schemas/in-toto-statement.schema.json +32 -0
- package/schemas/responsibility.schema.json +37 -0
- package/schemas/verification-scope.schema.json +26 -0
- package/schemas/workspace-binding.schema.json +20 -0
- package/scripts/CI_VALIDATORS.md +3 -0
- package/scripts/benchmark-cli-startup.mjs +60 -0
- package/scripts/check-changelog-freshness.mjs +88 -0
- package/scripts/check-critical-coverage.mjs +58 -0
- package/scripts/generate-agent-protocol-summary.mjs +141 -0
- package/scripts/generate-shell-completions.mjs +132 -0
- package/scripts/write-forgeloop-attestation-summary.mjs +93 -0
- package/src/cli.js +114 -6
- package/src/commands/attestation-create.js +30 -0
- package/src/commands/attestation-status.js +12 -0
- package/src/commands/attestation-verify-range.js +32 -0
- package/src/commands/attestation-verify.js +23 -0
- package/src/commands/handoff-create.js +9 -0
- package/src/commands/handoff-list.js +12 -0
- package/src/commands/handoff-show.js +10 -0
- package/src/commands/responsibility-set.js +20 -0
- package/src/commands/responsibility-status.js +12 -0
- package/src/commands/run-check.js +43 -3
- package/src/commands/task-repair-legacy-recovery.js +1 -2
- package/src/commands/verify-scope.js +9 -0
- package/src/commands/workspace-bind.js +17 -0
- package/src/commands/workspace-status.js +19 -0
- package/src/core/artifact-registry.js +84 -0
- package/src/core/attestation-coverage.js +134 -0
- package/src/core/attestation-verifier.js +227 -0
- package/src/core/attestation.js +263 -0
- package/src/core/audit.js +67 -1
- package/src/core/bundles.js +210 -9
- package/src/core/cli-command-definitions.js +196 -0
- package/src/core/code-manifest.js +293 -0
- package/src/core/command-executors.js +79 -0
- package/src/core/command-input.js +69 -18
- package/src/core/command-runtime.js +2 -1
- package/src/core/completion.js +170 -8
- package/src/core/config.js +60 -2
- package/src/core/continuity-cli-options.js +9 -3
- package/src/core/continuity-reconciliation.js +20 -0
- package/src/core/diagnostic-projection.js +3 -1
- package/src/core/error-codes.js +146 -0
- package/src/core/events.js +109 -13
- package/src/core/exit-codes.js +22 -0
- package/src/core/handoff.js +208 -0
- package/src/core/information-gain-projection.js +3 -3
- package/src/core/integration-invocation-policy.js +40 -0
- package/src/core/integration-resources.js +54 -0
- package/src/core/next-action-phases.js +968 -0
- package/src/core/next-action.js +32 -951
- package/src/core/phase.js +29 -1
- package/src/core/protocol-info.js +41 -1
- package/src/core/repository.js +256 -11
- package/src/core/responsibility.js +265 -0
- package/src/core/revision/git.js +207 -0
- package/src/core/revision/provider.js +90 -0
- package/src/core/revision/registry.js +5 -0
- package/src/core/route-artifact.js +1 -1
- package/src/core/schema-validation.js +8 -0
- package/src/core/signing/none.js +23 -0
- package/src/core/signing/provider.js +21 -0
- package/src/core/signing/registry.js +7 -0
- package/src/core/signing/sigstore.js +158 -0
- package/src/core/task-command.js +9 -1
- package/src/core/task-paths.js +72 -0
- package/src/core/templates.js +8 -0
- package/src/core/verification-scope-capability.js +179 -0
- package/src/core/verification-scope.js +271 -0
- package/src/core/workspace-binding.js +211 -0
- package/src/integration.d.ts +91 -0
- package/AGENT_COMPATIBILITY.md +0 -11
- package/docs/RELEASE_CHECKLIST_1_4.md +0 -38
- package/docs/RELEASE_CHECKLIST_1_5_MCP.md +0 -78
- package/docs/RELEASE_CHECKLIST_1_6_1.md +0 -121
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { readFile, writeFile } from "node:fs/promises";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
|
|
7
|
+
import { ARTIFACT_REGISTRY } from "../src/core/artifact-registry.js";
|
|
8
|
+
import { CLI_COMMAND_DEFINITIONS } from "../src/core/cli-command-definitions.js";
|
|
9
|
+
import { PUBLIC_ERROR_REGISTRY } from "../src/core/error-codes.js";
|
|
10
|
+
import { protocolInfo } from "../src/core/protocol-info.js";
|
|
11
|
+
|
|
12
|
+
const repositoryRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
13
|
+
const outputPath = path.join(repositoryRoot, "docs", "AGENT_PROTOCOL_SUMMARY.md");
|
|
14
|
+
|
|
15
|
+
function cell(value) {
|
|
16
|
+
const normalized = String(value ?? "");
|
|
17
|
+
return (normalized || "n/a").replaceAll("|", "\\|").replaceAll("\n", " ");
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function table(headers, rows) {
|
|
21
|
+
const lines = [
|
|
22
|
+
`| ${headers.map(cell).join(" | ")} |`,
|
|
23
|
+
`| ${headers.map(() => "---").join(" | ")} |`,
|
|
24
|
+
];
|
|
25
|
+
for (const row of rows) lines.push(`| ${row.map(cell).join(" | ")} |`);
|
|
26
|
+
return lines.join("\n");
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function commandCatalog() {
|
|
30
|
+
const groups = new Map();
|
|
31
|
+
for (const command of Object.values(CLI_COMMAND_DEFINITIONS).sort((left, right) => left.name.localeCompare(right.name))) {
|
|
32
|
+
const commands = groups.get(command.category) ?? [];
|
|
33
|
+
commands.push(command);
|
|
34
|
+
groups.set(command.category, commands);
|
|
35
|
+
}
|
|
36
|
+
return [...groups.entries()]
|
|
37
|
+
.sort(([left], [right]) => left.localeCompare(right))
|
|
38
|
+
.map(([category, commands]) => [
|
|
39
|
+
`### ${category}`,
|
|
40
|
+
table(["Command", "Mutation", "Purpose"], commands.map((command) => [command.name, command.mutation, command.description])),
|
|
41
|
+
])
|
|
42
|
+
.flat()
|
|
43
|
+
.join("\n\n");
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async function render() {
|
|
47
|
+
const packageJson = JSON.parse(await readFile(path.join(repositoryRoot, "package.json"), "utf8"));
|
|
48
|
+
const info = protocolInfo({ packageVersion: packageJson.version });
|
|
49
|
+
const features = Object.entries(info.features)
|
|
50
|
+
.sort(([left], [right]) => left.localeCompare(right))
|
|
51
|
+
.map(([name, feature]) => [name, feature.version ?? "n/a", feature.supported === false ? "no" : "yes"]);
|
|
52
|
+
const artifacts = Object.values(ARTIFACT_REGISTRY)
|
|
53
|
+
.filter((artifact) => artifact.isPublic)
|
|
54
|
+
.sort((left, right) => left.key.localeCompare(right.key))
|
|
55
|
+
.map((artifact) => [artifact.key, artifact.scope, artifact.path, artifact.schema, artifact.trustRole]);
|
|
56
|
+
const errors = Object.values(PUBLIC_ERROR_REGISTRY)
|
|
57
|
+
.filter((error) => /^(E_(CLI|WORKSPACE|HANDOFF|RESPONSIBILITY|VERIFICATION_SCOPE|REVISION|ATTESTATION)_)/u.test(error.code))
|
|
58
|
+
.sort((left, right) => left.code.localeCompare(right.code))
|
|
59
|
+
.map((error) => [error.code, error.meaning]);
|
|
60
|
+
|
|
61
|
+
return `# ForgeLoop Agent Protocol Summary
|
|
62
|
+
|
|
63
|
+
> Generated from the ForgeLoop protocol registries. This file is a concise navigation aid; the normative documents, schemas, and CLI implementation remain authoritative.
|
|
64
|
+
|
|
65
|
+
## Scope
|
|
66
|
+
|
|
67
|
+
ForgeLoop is a portable protocol and support CLI for verifiable engineering workflows. It records and validates task state, contracts, routing, checks, evidence, continuity, and optional code attestations. It does not become an agent scheduler, delegation service, source-control authority, or secret manager.
|
|
68
|
+
|
|
69
|
+
Protocol version: ${info.protocolVersion}
|
|
70
|
+
Package version: ${packageJson.version}
|
|
71
|
+
|
|
72
|
+
## Canonical loop
|
|
73
|
+
|
|
74
|
+
1. Discover existing work with forgeloop task-list --json.
|
|
75
|
+
2. Select or create one task, then run forgeloop next --task <id> --json.
|
|
76
|
+
3. Persist the contract, route, gates, and source registry; require forgeloop preflight to return READY.
|
|
77
|
+
4. Execute bounded checks through the canonical CLI and preserve execution provenance.
|
|
78
|
+
5. Reconcile continuity, inspect evidence, and advance through VERIFYING and REVIEWING.
|
|
79
|
+
6. Run forgeloop complete; accept completion only when the validator returns VALID.
|
|
80
|
+
7. Run forgeloop next again and follow the returned lifecycle action to a terminal state or an explicit blocker.
|
|
81
|
+
|
|
82
|
+
## Authority boundaries
|
|
83
|
+
|
|
84
|
+
- Protocol-derived facts outrank actor-provided labels, free-form summaries, and guessed identities.
|
|
85
|
+
- Optional artifacts are reported as NOT_APPLICABLE when absent; malformed active security artifacts fail closed.
|
|
86
|
+
- Workspace binding confirms an explicit Git worktree identity but is not a portability requirement for attestation.
|
|
87
|
+
- Handoff envelopes record immutable intent and state snapshots; they do not delegate work or grant completion authority.
|
|
88
|
+
- Responsibility contracts constrain paths, checks, and frozen inputs; they do not prove identity or authorship.
|
|
89
|
+
- Verification scope describes planned verification breadth. Attestation coverage proves content for a concrete revision. These are separate claims.
|
|
90
|
+
- Attestation manifests exclude ForgeLoop protocol metadata and bind to the completion receipt and append-only ledger without circular references.
|
|
91
|
+
- Verification commands are read-only. Signing is external; private keys and credentials are never persisted by ForgeLoop.
|
|
92
|
+
|
|
93
|
+
## Lifecycle
|
|
94
|
+
|
|
95
|
+
Phases: ${info.lifecycle.phases.join(", ")}
|
|
96
|
+
|
|
97
|
+
## Feature registry
|
|
98
|
+
|
|
99
|
+
${table(["Feature", "Version", "Supported"], features)}
|
|
100
|
+
|
|
101
|
+
## Public artifact registry
|
|
102
|
+
|
|
103
|
+
${table(["Key", "Scope", "Path", "Schema", "Trust role"], artifacts)}
|
|
104
|
+
|
|
105
|
+
## Command catalog
|
|
106
|
+
|
|
107
|
+
${commandCatalog()}
|
|
108
|
+
|
|
109
|
+
## Stable boundary and attestation errors
|
|
110
|
+
|
|
111
|
+
${table(["Code", "Meaning"], errors)}
|
|
112
|
+
|
|
113
|
+
## Authoritative implementation surfaces
|
|
114
|
+
|
|
115
|
+
- Normative protocol: LOOP_ENGINEERING.md and PROTOCOL_INTEGRATION.md.
|
|
116
|
+
- Artifact contracts: schemas/.
|
|
117
|
+
- Lifecycle and command behavior: src/core/ and src/cli.js.
|
|
118
|
+
- Integration surface: src/integration.js, src/integration.d.ts, and integrations/.
|
|
119
|
+
- User-facing command and artifact references: docs/CLI_REFERENCE.md and docs/ARTIFACT_REFERENCE.md.
|
|
120
|
+
`;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const check = process.argv.includes("--check");
|
|
124
|
+
const content = await render();
|
|
125
|
+
let current = null;
|
|
126
|
+
try {
|
|
127
|
+
current = await readFile(outputPath, "utf8");
|
|
128
|
+
} catch (error) {
|
|
129
|
+
if (error.code !== "ENOENT") throw error;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (check) {
|
|
133
|
+
if (current !== content) {
|
|
134
|
+
console.error(`Generated agent summary is stale: ${path.relative(repositoryRoot, outputPath)}`);
|
|
135
|
+
process.exit(1);
|
|
136
|
+
}
|
|
137
|
+
console.log(`Generated agent summary is current: ${path.relative(repositoryRoot, outputPath)}`);
|
|
138
|
+
} else {
|
|
139
|
+
await writeFile(outputPath, content, "utf8");
|
|
140
|
+
console.log(`Generated ${path.relative(repositoryRoot, outputPath)}`);
|
|
141
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { readFile, writeFile } from "node:fs/promises";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
|
|
7
|
+
import { CLI_COMMAND_DEFINITIONS } from "../src/core/cli-command-definitions.js";
|
|
8
|
+
|
|
9
|
+
const repositoryRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
10
|
+
const completionDirectory = path.join(repositoryRoot, "completions");
|
|
11
|
+
const commands = Object.keys(CLI_COMMAND_DEFINITIONS).sort();
|
|
12
|
+
|
|
13
|
+
function shellSingleQuote(value) {
|
|
14
|
+
return `'${String(value).replaceAll("'", "'\\\"'\\\"'")}'`;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function optionsFor(command) {
|
|
18
|
+
return Object.keys(CLI_COMMAND_DEFINITIONS[command].options)
|
|
19
|
+
.filter((name) => name.startsWith("--"))
|
|
20
|
+
.sort();
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function bashCompletion() {
|
|
24
|
+
const commandWords = commands.join(" ");
|
|
25
|
+
const commandCases = commands
|
|
26
|
+
.map((command) => `${command}) command="${command}" ;;`)
|
|
27
|
+
.join("\n ");
|
|
28
|
+
const optionCases = commands
|
|
29
|
+
.map((command) => ` ${command}) COMPREPLY=( $(compgen -W ${shellSingleQuote(optionsFor(command).join(" "))} -- "$cur") );;`)
|
|
30
|
+
.join("\n");
|
|
31
|
+
return [
|
|
32
|
+
"# Generated by scripts/generate-shell-completions.mjs. Do not edit.",
|
|
33
|
+
"_forgeloop() {",
|
|
34
|
+
" local cur command",
|
|
35
|
+
" COMPREPLY=()",
|
|
36
|
+
" cur=\"\\${COMP_WORDS[COMP_CWORD]}\"",
|
|
37
|
+
" command=\"\"",
|
|
38
|
+
" for word in \"\\${COMP_WORDS[@]:1}\"; do",
|
|
39
|
+
" case \"\\${word}\" in",
|
|
40
|
+
` ${commandCases}`,
|
|
41
|
+
" esac",
|
|
42
|
+
" done",
|
|
43
|
+
" if [[ -z \"\\${command}\" && \"\\${cur}\" != -* ]]; then",
|
|
44
|
+
` COMPREPLY=( $(compgen -W ${shellSingleQuote(commandWords)} -- "$cur") )`,
|
|
45
|
+
" return",
|
|
46
|
+
" fi",
|
|
47
|
+
" case \"\\${command}\" in",
|
|
48
|
+
optionCases,
|
|
49
|
+
` *) COMPREPLY=( $(compgen -W ${shellSingleQuote("--help --version --path")} -- "$cur") ) ;;`,
|
|
50
|
+
" esac",
|
|
51
|
+
"}",
|
|
52
|
+
"complete -F _forgeloop forgeloop",
|
|
53
|
+
"",
|
|
54
|
+
].join("\n");
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function zshCompletion() {
|
|
58
|
+
const commandCaseLines = commands
|
|
59
|
+
.map((command) => {
|
|
60
|
+
const options = optionsFor(command)
|
|
61
|
+
.map((option) => shellSingleQuote(`${option}[${CLI_COMMAND_DEFINITIONS[command].options[option].description}]`))
|
|
62
|
+
.join(" ");
|
|
63
|
+
return ` ${command}) _arguments ${options} ;;`;
|
|
64
|
+
})
|
|
65
|
+
.join("\n");
|
|
66
|
+
return [
|
|
67
|
+
"#compdef forgeloop",
|
|
68
|
+
"# Generated by scripts/generate-shell-completions.mjs. Do not edit.",
|
|
69
|
+
`_arguments \\
|
|
70
|
+
'1:command:(${commands.join(" ")})' \\
|
|
71
|
+
'*::options:->options'`,
|
|
72
|
+
"case $words[2] in",
|
|
73
|
+
commandCaseLines,
|
|
74
|
+
" *) _arguments '--help[show this help]' '--version[show the installed package version]' '--path[target project directory]' ;;",
|
|
75
|
+
"esac",
|
|
76
|
+
"",
|
|
77
|
+
].join("\n");
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function fishCompletion() {
|
|
81
|
+
const lines = [
|
|
82
|
+
"# Generated by scripts/generate-shell-completions.mjs. Do not edit.",
|
|
83
|
+
`complete -c forgeloop -f -n '__fish_use_subcommand' -a '${commands.join(" ")}'`,
|
|
84
|
+
];
|
|
85
|
+
for (const command of commands) {
|
|
86
|
+
for (const option of optionsFor(command)) {
|
|
87
|
+
const description = CLI_COMMAND_DEFINITIONS[command].options[option].description.replaceAll("'", "\\'");
|
|
88
|
+
lines.push(`complete -c forgeloop -f -n '__fish_seen_subcommand_from ${command}' -l '${option.slice(2)}' -d '${description}'`);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
lines.push("complete -c forgeloop -f -l help -s h -d 'show this help'");
|
|
92
|
+
lines.push("complete -c forgeloop -f -l version -s v -d 'show the installed package version'");
|
|
93
|
+
return `${lines.join("\n")}\n`;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const outputs = new Map([
|
|
97
|
+
["forgeloop.bash", bashCompletion()],
|
|
98
|
+
["_forgeloop", zshCompletion()],
|
|
99
|
+
["forgeloop.fish", fishCompletion()],
|
|
100
|
+
]);
|
|
101
|
+
|
|
102
|
+
export async function generateShellCompletions({ write = false } = {}) {
|
|
103
|
+
const results = [];
|
|
104
|
+
for (const [name, expected] of outputs) {
|
|
105
|
+
const filePath = path.join(completionDirectory, name);
|
|
106
|
+
let actual = null;
|
|
107
|
+
try {
|
|
108
|
+
actual = await readFile(filePath, "utf8");
|
|
109
|
+
} catch {
|
|
110
|
+
// A missing generated file is reported below.
|
|
111
|
+
}
|
|
112
|
+
if (write) {
|
|
113
|
+
await writeFile(filePath, expected, "utf8");
|
|
114
|
+
results.push({ name, status: "written" });
|
|
115
|
+
} else {
|
|
116
|
+
results.push({ name, status: actual === expected ? "current" : "stale" });
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return results;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
|
123
|
+
const write = process.argv.includes("--write");
|
|
124
|
+
const results = await generateShellCompletions({ write });
|
|
125
|
+
const stale = results.filter((item) => item.status === "stale");
|
|
126
|
+
if (stale.length > 0) {
|
|
127
|
+
console.error(`Generated shell completions are stale: ${stale.map((item) => item.name).join(", ")}`);
|
|
128
|
+
process.exitCode = 1;
|
|
129
|
+
} else {
|
|
130
|
+
console.log(write ? "Generated shell completions." : "Shell completions are current.");
|
|
131
|
+
}
|
|
132
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { readFile, writeFile } from "node:fs/promises";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
|
|
6
|
+
function usage() {
|
|
7
|
+
return [
|
|
8
|
+
"Usage: node scripts/write-forgeloop-attestation-summary.mjs --input <result.json> [--output <summary.md>]",
|
|
9
|
+
"",
|
|
10
|
+
"When --output is omitted, GITHUB_STEP_SUMMARY is used when available; otherwise the summary is written to stdout.",
|
|
11
|
+
].join("\n");
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function parseArgs(argv) {
|
|
15
|
+
const options = { input: null, output: null };
|
|
16
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
17
|
+
const argument = argv[index];
|
|
18
|
+
if (argument === "--help" || argument === "-h") {
|
|
19
|
+
console.log(usage());
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
if (argument === "--input" || argument === "--output") {
|
|
23
|
+
const value = argv[index + 1];
|
|
24
|
+
if (!value || value.startsWith("-")) throw new Error(`${argument} requires a value`);
|
|
25
|
+
options[argument.slice(2)] = value;
|
|
26
|
+
index += 1;
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
throw new Error(`Unknown option: ${argument}`);
|
|
30
|
+
}
|
|
31
|
+
if (!options.input) throw new Error("--input is required");
|
|
32
|
+
return options;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function safeNumber(value) {
|
|
36
|
+
return Number.isFinite(value) ? value : 0;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function summaryFor(result) {
|
|
40
|
+
if (!result || typeof result !== "object" || Array.isArray(result)) {
|
|
41
|
+
throw new Error("The attestation result must be a JSON object");
|
|
42
|
+
}
|
|
43
|
+
const changed = safeNumber(result.changedPaths);
|
|
44
|
+
const covered = safeNumber(result.coveredPaths);
|
|
45
|
+
const uncovered = Array.isArray(result.uncoveredPaths) ? result.uncoveredPaths.length : 0;
|
|
46
|
+
const signature = result.signature && typeof result.signature === "object"
|
|
47
|
+
? result.signature
|
|
48
|
+
: {};
|
|
49
|
+
const errorCodes = Array.isArray(result.errors)
|
|
50
|
+
? [...new Set(result.errors.map((error) => error?.code).filter(Boolean))].sort()
|
|
51
|
+
: [];
|
|
52
|
+
const lines = [
|
|
53
|
+
"## ForgeLoop Attestation",
|
|
54
|
+
"",
|
|
55
|
+
`- **Status:** ${String(result.status ?? "UNKNOWN")}`,
|
|
56
|
+
`- **Level:** ${String(result.level ?? "UNKNOWN")}`,
|
|
57
|
+
`- **Revision provider:** ${String(result.revisionProvider ?? "unknown")}`,
|
|
58
|
+
`- **Base:** ${String(result.baseRevision ?? "unknown")}`,
|
|
59
|
+
`- **Head:** ${String(result.headRevision ?? "unknown")}`,
|
|
60
|
+
`- **Coverage:** ${covered} / ${changed} changed paths`,
|
|
61
|
+
`- **Uncovered paths:** ${uncovered}`,
|
|
62
|
+
`- **Tasks:** ${safeNumber(result.tasks)}`,
|
|
63
|
+
`- **Signature:** ${String(signature.status ?? "NOT_CHECKED")}`,
|
|
64
|
+
];
|
|
65
|
+
if (errorCodes.length > 0) {
|
|
66
|
+
lines.push("", "**Stable error codes**", "", ...errorCodes.map((code) => `- \`${code}\``));
|
|
67
|
+
}
|
|
68
|
+
lines.push("");
|
|
69
|
+
return lines.join("\n");
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async function main(argv = process.argv.slice(2)) {
|
|
73
|
+
const options = parseArgs(argv);
|
|
74
|
+
if (!options) return 0;
|
|
75
|
+
const result = JSON.parse(await readFile(path.resolve(options.input), "utf8"));
|
|
76
|
+
const summary = summaryFor(result);
|
|
77
|
+
const destination = options.output ?? process.env.GITHUB_STEP_SUMMARY;
|
|
78
|
+
if (destination) {
|
|
79
|
+
await writeFile(path.resolve(destination), `${summary}\n`, "utf8");
|
|
80
|
+
console.log(`Wrote ForgeLoop attestation summary to ${destination}`);
|
|
81
|
+
} else {
|
|
82
|
+
process.stdout.write(`${summary}\n`);
|
|
83
|
+
}
|
|
84
|
+
return 0;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
try {
|
|
88
|
+
process.exitCode = await main();
|
|
89
|
+
} catch (error) {
|
|
90
|
+
console.error(`Attestation summary failed: ${error.message}`);
|
|
91
|
+
console.error(usage());
|
|
92
|
+
process.exitCode = 2;
|
|
93
|
+
}
|
package/src/cli.js
CHANGED
|
@@ -67,11 +67,25 @@ import {
|
|
|
67
67
|
} from "./commands/task-repair-legacy-recovery.js";
|
|
68
68
|
import { formatTaskLockStatusResult } from "./commands/task-lock-status.js";
|
|
69
69
|
import { formatProtocolInfoResult } from "./commands/protocol-info.js";
|
|
70
|
+
import { formatWorkspaceBindResult } from "./commands/workspace-bind.js";
|
|
71
|
+
import { formatWorkspaceStatusResult } from "./commands/workspace-status.js";
|
|
72
|
+
import { formatHandoffCreateResult } from "./commands/handoff-create.js";
|
|
73
|
+
import { formatHandoffListResult } from "./commands/handoff-list.js";
|
|
74
|
+
import { formatHandoffShowResult } from "./commands/handoff-show.js";
|
|
75
|
+
import { formatResponsibilitySetResult } from "./commands/responsibility-set.js";
|
|
76
|
+
import { formatResponsibilityStatusResult } from "./commands/responsibility-status.js";
|
|
77
|
+
import { formatVerifyScopeResult } from "./commands/verify-scope.js";
|
|
78
|
+
import { formatAttestationCreateResult } from "./commands/attestation-create.js";
|
|
79
|
+
import { formatAttestationVerifyResult } from "./commands/attestation-verify.js";
|
|
80
|
+
import { formatAttestationStatusResult } from "./commands/attestation-status.js";
|
|
81
|
+
import { formatAttestationVerifyRangeResult } from "./commands/attestation-verify-range.js";
|
|
70
82
|
import { defaultCommandInputValues, validateForgeLoopCommandInput } from "./core/command-input.js";
|
|
71
83
|
import { COMMAND_EXECUTORS } from "./core/command-executors.js";
|
|
72
84
|
import { resolveTarget } from "./core/filesystem.js";
|
|
73
85
|
import { getPackageRoot } from "./core/templates.js";
|
|
74
86
|
import { CLI_COMMAND_DEFINITIONS, buildOptionLookup, getPositionalDefinitions } from "./core/cli-command-definitions.js";
|
|
87
|
+
import { exitCodeForError } from "./core/exit-codes.js";
|
|
88
|
+
import { E_CLI_INVOCATION_INVALID } from "./core/error-codes.js";
|
|
75
89
|
|
|
76
90
|
export const COMMANDS = Object.freeze(Object.keys(CLI_COMMAND_DEFINITIONS));
|
|
77
91
|
|
|
@@ -295,9 +309,14 @@ export function validateCliSemantics({ command, options } = {}) {
|
|
|
295
309
|
}
|
|
296
310
|
|
|
297
311
|
export function parseArgs(argv) {
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
312
|
+
try {
|
|
313
|
+
const parsed = parseCliSyntax(argv);
|
|
314
|
+
validateCliSemantics(parsed);
|
|
315
|
+
return parsed;
|
|
316
|
+
} catch (error) {
|
|
317
|
+
if (!error.code) error.code = E_CLI_INVOCATION_INVALID;
|
|
318
|
+
throw error;
|
|
319
|
+
}
|
|
301
320
|
}
|
|
302
321
|
|
|
303
322
|
async function packageVersion(packageRoot) {
|
|
@@ -337,7 +356,9 @@ export const COMMAND_HANDLERS = Object.freeze({
|
|
|
337
356
|
for (const item of result.findings) {
|
|
338
357
|
console.log(`${item.severity}: ${item.code}: ${item.path} - ${item.message}`);
|
|
339
358
|
}
|
|
340
|
-
console.log(result.ok
|
|
359
|
+
console.log(result.ok
|
|
360
|
+
? "healthy: ForgeLoop target is ready; project profile is available"
|
|
361
|
+
: "unhealthy: ForgeLoop target needs attention");
|
|
341
362
|
}
|
|
342
363
|
return result.ok ? 0 : 1;
|
|
343
364
|
},
|
|
@@ -396,6 +417,66 @@ export const COMMAND_HANDLERS = Object.freeze({
|
|
|
396
417
|
renderJsonOr(options, result, formatRunCheckResult);
|
|
397
418
|
return exitCode;
|
|
398
419
|
},
|
|
420
|
+
"workspace-bind": async ({ target, packageRoot, options }) => {
|
|
421
|
+
const { result, exitCode } = await COMMAND_EXECUTORS["workspace-bind"]({ target, packageRoot, options });
|
|
422
|
+
renderJsonOr(options, result, formatWorkspaceBindResult);
|
|
423
|
+
return exitCode;
|
|
424
|
+
},
|
|
425
|
+
"workspace-status": async ({ target, packageRoot, options }) => {
|
|
426
|
+
const { result, exitCode } = await COMMAND_EXECUTORS["workspace-status"]({ target, packageRoot, options });
|
|
427
|
+
renderJsonOr(options, result, formatWorkspaceStatusResult);
|
|
428
|
+
return exitCode;
|
|
429
|
+
},
|
|
430
|
+
"handoff-create": async ({ target, packageRoot, options }) => {
|
|
431
|
+
const { result, exitCode } = await COMMAND_EXECUTORS["handoff-create"]({ target, packageRoot, options });
|
|
432
|
+
renderJsonOr(options, result, formatHandoffCreateResult);
|
|
433
|
+
return exitCode;
|
|
434
|
+
},
|
|
435
|
+
"handoff-list": async ({ target, packageRoot, options }) => {
|
|
436
|
+
const { result, exitCode } = await COMMAND_EXECUTORS["handoff-list"]({ target, packageRoot, options });
|
|
437
|
+
renderJsonOr(options, result, formatHandoffListResult);
|
|
438
|
+
return exitCode;
|
|
439
|
+
},
|
|
440
|
+
"handoff-show": async ({ target, packageRoot, options }) => {
|
|
441
|
+
const { result, exitCode } = await COMMAND_EXECUTORS["handoff-show"]({ target, packageRoot, options });
|
|
442
|
+
renderJsonOr(options, result, formatHandoffShowResult);
|
|
443
|
+
return exitCode;
|
|
444
|
+
},
|
|
445
|
+
"responsibility-set": async ({ target, packageRoot, options }) => {
|
|
446
|
+
const { result, exitCode } = await COMMAND_EXECUTORS["responsibility-set"]({ target, packageRoot, options });
|
|
447
|
+
renderJsonOr(options, result, formatResponsibilitySetResult);
|
|
448
|
+
return exitCode;
|
|
449
|
+
},
|
|
450
|
+
"responsibility-status": async ({ target, packageRoot, options }) => {
|
|
451
|
+
const { result, exitCode } = await COMMAND_EXECUTORS["responsibility-status"]({ target, packageRoot, options });
|
|
452
|
+
renderJsonOr(options, result, formatResponsibilityStatusResult);
|
|
453
|
+
return exitCode;
|
|
454
|
+
},
|
|
455
|
+
"verify-scope": async ({ target, packageRoot, options }) => {
|
|
456
|
+
const { result, exitCode } = await COMMAND_EXECUTORS["verify-scope"]({ target, packageRoot, options });
|
|
457
|
+
renderJsonOr(options, result, formatVerifyScopeResult);
|
|
458
|
+
return exitCode;
|
|
459
|
+
},
|
|
460
|
+
"attestation-create": async ({ target, packageRoot, options }) => {
|
|
461
|
+
const { result, exitCode } = await COMMAND_EXECUTORS["attestation-create"]({ target, packageRoot, options });
|
|
462
|
+
renderJsonOr(options, result, formatAttestationCreateResult);
|
|
463
|
+
return exitCode;
|
|
464
|
+
},
|
|
465
|
+
"attestation-verify": async ({ target, packageRoot, options }) => {
|
|
466
|
+
const { result, exitCode } = await COMMAND_EXECUTORS["attestation-verify"]({ target, packageRoot, options });
|
|
467
|
+
renderJsonOr(options, result, formatAttestationVerifyResult);
|
|
468
|
+
return exitCode;
|
|
469
|
+
},
|
|
470
|
+
"attestation-status": async ({ target, packageRoot, options }) => {
|
|
471
|
+
const { result, exitCode } = await COMMAND_EXECUTORS["attestation-status"]({ target, packageRoot, options });
|
|
472
|
+
renderJsonOr(options, result, formatAttestationStatusResult);
|
|
473
|
+
return exitCode;
|
|
474
|
+
},
|
|
475
|
+
"attestation-verify-range": async ({ target, packageRoot, options }) => {
|
|
476
|
+
const { result, exitCode } = await COMMAND_EXECUTORS["attestation-verify-range"]({ target, packageRoot, options });
|
|
477
|
+
renderJsonOr(options, result, formatAttestationVerifyRangeResult);
|
|
478
|
+
return exitCode;
|
|
479
|
+
},
|
|
399
480
|
"run-action": async ({ target, packageRoot, options }) => {
|
|
400
481
|
const { result, exitCode } = await COMMAND_EXECUTORS["run-action"]({ target, packageRoot, options });
|
|
401
482
|
renderJsonOr(options, result, formatRunActionResult);
|
|
@@ -667,7 +748,17 @@ export const COMMAND_TABLE = Object.freeze(
|
|
|
667
748
|
);
|
|
668
749
|
|
|
669
750
|
export async function main(argv = process.argv.slice(2)) {
|
|
751
|
+
const jsonRequested = argv.some((argument) => argument === "--json" || argument.startsWith("--json="));
|
|
752
|
+
const jsonErrorsAllowed = jsonRequested;
|
|
670
753
|
try {
|
|
754
|
+
if (argv[0] === "help") {
|
|
755
|
+
const helpCommand = argv[1] ?? null;
|
|
756
|
+
if (helpCommand && !CLI_COMMAND_DEFINITIONS[helpCommand]) {
|
|
757
|
+
throw new Error(`Unknown command for help: ${helpCommand}`);
|
|
758
|
+
}
|
|
759
|
+
console.log(usage(helpCommand));
|
|
760
|
+
return helpCommand ? 0 : 1;
|
|
761
|
+
}
|
|
671
762
|
const { command, options } = parseArgs(argv);
|
|
672
763
|
if (options.version) {
|
|
673
764
|
console.log(await packageVersion(getPackageRoot()));
|
|
@@ -693,8 +784,25 @@ export async function main(argv = process.argv.slice(2)) {
|
|
|
693
784
|
options,
|
|
694
785
|
});
|
|
695
786
|
} catch (error) {
|
|
696
|
-
|
|
697
|
-
|
|
787
|
+
if (jsonErrorsAllowed) {
|
|
788
|
+
const payload = JSON.stringify({
|
|
789
|
+
status: "ERROR",
|
|
790
|
+
ok: false,
|
|
791
|
+
error: {
|
|
792
|
+
code: error.code ?? "E_CLI_INVOCATION_INVALID",
|
|
793
|
+
message: error.message,
|
|
794
|
+
...(error.next ? { next: error.next } : {}),
|
|
795
|
+
...(error.artifacts ? { artifacts: error.artifacts } : {}),
|
|
796
|
+
},
|
|
797
|
+
}, null, 2);
|
|
798
|
+
// Keep stderr as the diagnostic channel while also emitting the same
|
|
799
|
+
// structured envelope on stdout for machine consumers.
|
|
800
|
+
console.log(payload);
|
|
801
|
+
console.error(payload);
|
|
802
|
+
} else {
|
|
803
|
+
console.error(`error: ${error.code ? `${error.code}: ` : ""}${error.message}`);
|
|
804
|
+
}
|
|
805
|
+
return exitCodeForError(error);
|
|
698
806
|
}
|
|
699
807
|
}
|
|
700
808
|
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { buildAttestationStatement, writeAttestationStatement } from "../core/attestation.js";
|
|
2
|
+
import { withTaskTransaction } from "../core/transaction.js";
|
|
3
|
+
import { assertTaskMutationAllowed } from "../core/task-claim-state.js";
|
|
4
|
+
import { readWorkState } from "../core/work-state.js";
|
|
5
|
+
import { assertWorkspaceBinding } from "../core/workspace-binding.js";
|
|
6
|
+
|
|
7
|
+
export async function runAttestationCreate({ target, packageRoot, taskId } = {}) {
|
|
8
|
+
return withTaskTransaction({ target, taskId, packageRoot, operation: "attestation-create", recordCommitEvent: true }, async () => {
|
|
9
|
+
const state = await readWorkState(target, { packageRoot, taskId });
|
|
10
|
+
if (!state || state.phase !== "COMPLETE") {
|
|
11
|
+
const error = new Error("attestation-create requires a COMPLETE task");
|
|
12
|
+
error.code = "E_ATTESTATION_STATEMENT_INVALID";
|
|
13
|
+
throw error;
|
|
14
|
+
}
|
|
15
|
+
// COMPLETE releases ordinary write claims, but a statement is a
|
|
16
|
+
// protocol-owned derivative of that terminal state. Keep this command
|
|
17
|
+
// terminal-safe while refusing all ordinary task mutations.
|
|
18
|
+
await assertTaskMutationAllowed(target, { taskId, packageRoot }).catch((error) => {
|
|
19
|
+
if (error.code !== "E_TASK_COMPLETE") throw error;
|
|
20
|
+
});
|
|
21
|
+
await assertWorkspaceBinding(target, { taskId, packageRoot, operation: "attestation-create" });
|
|
22
|
+
const built = await buildAttestationStatement({ target, packageRoot, taskId });
|
|
23
|
+
const written = await writeAttestationStatement({ target, packageRoot, taskId, statement: built.statement });
|
|
24
|
+
return { ...written, statementFingerprint: built.fingerprint, manifestFingerprint: built.manifest.fingerprint };
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function formatAttestationCreateResult(result) {
|
|
29
|
+
return `FORGELOOP ATTESTATION CREATED\ntask: ${result.statement.predicate.task.taskId}\nfingerprint: ${result.statementFingerprint}\npath: ${result.path}\nsubject: sha256:${result.statement.subject[0].digest.sha256}\n\n`;
|
|
30
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { resolveAttestationStatus } from "../core/attestation.js";
|
|
2
|
+
|
|
3
|
+
export async function runAttestationStatus({ target, packageRoot, taskId } = {}) {
|
|
4
|
+
return resolveAttestationStatus({ target, packageRoot, taskId });
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function formatAttestationStatusResult(result) {
|
|
8
|
+
const lines = [`FORGELOOP ATTESTATION: ${result.status}`, `TASK: ${result.taskId}`, `LEVEL: ${result.level}`, `CONTENT: ${result.content}`, `RECEIPT: ${result.receipt}`, `LEDGER: ${result.ledger}`, `SIGNATURE: ${result.signature}`, `FILES: ${result.files}`];
|
|
9
|
+
if (result.subject) lines.push(`SUBJECT: ${result.subject}`);
|
|
10
|
+
for (const error of result.errors ?? []) lines.push(`${error.code}: ${error.message}`);
|
|
11
|
+
return `${lines.join("\n")}\n`;
|
|
12
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { evaluateAttestationCoverage } from "../core/attestation-coverage.js";
|
|
2
|
+
|
|
3
|
+
export async function runAttestationVerifyRange(options = {}) {
|
|
4
|
+
return evaluateAttestationCoverage({
|
|
5
|
+
target: options.target,
|
|
6
|
+
packageRoot: options.packageRoot,
|
|
7
|
+
revisionProvider: options.revisionProvider ?? "git",
|
|
8
|
+
baseRevision: options.baseRevision,
|
|
9
|
+
headRevision: options.headRevision,
|
|
10
|
+
requireCompleteCoverage: options.requireCompleteCoverage === true,
|
|
11
|
+
requireSignature: options.requireSignature === true,
|
|
12
|
+
signingProvider: options.signingProvider,
|
|
13
|
+
signerPolicy: {
|
|
14
|
+
identity: options.attestationIdentity,
|
|
15
|
+
issuer: options.attestationIssuer,
|
|
16
|
+
trustedRoot: options.trustedRoot,
|
|
17
|
+
},
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function formatAttestationVerifyRangeResult(result) {
|
|
22
|
+
return [
|
|
23
|
+
`FORGELOOP REVISION VERIFICATION: ${result.status}`,
|
|
24
|
+
`LEVEL: ${result.level}`,
|
|
25
|
+
`CHANGED: ${result.changedPaths}`,
|
|
26
|
+
`COVERED: ${result.coveredPaths}`,
|
|
27
|
+
`UNCOVERED: ${result.uncoveredPaths.length}`,
|
|
28
|
+
`TASKS: ${result.tasks}`,
|
|
29
|
+
...result.errors.map((error) => `${error.code}: ${error.message}`),
|
|
30
|
+
"",
|
|
31
|
+
].join("\n");
|
|
32
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { verifyAttestation } from "../core/attestation-verifier.js";
|
|
2
|
+
|
|
3
|
+
export async function runAttestationVerify(options = {}) {
|
|
4
|
+
return verifyAttestation({
|
|
5
|
+
target: options.target,
|
|
6
|
+
packageRoot: options.packageRoot,
|
|
7
|
+
taskId: options.taskId,
|
|
8
|
+
revision: options.attestationRef,
|
|
9
|
+
bundlePath: options.attestationBundle,
|
|
10
|
+
identity: options.attestationIdentity,
|
|
11
|
+
issuer: options.attestationIssuer,
|
|
12
|
+
revisionProvider: options.revisionProvider,
|
|
13
|
+
signingProvider: options.signingProvider,
|
|
14
|
+
trustedRoot: options.trustedRoot,
|
|
15
|
+
requireSignature: options.requireSignature === true,
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function formatAttestationVerifyResult(result) {
|
|
20
|
+
const lines = [`FORGELOOP ATTESTATION: ${result.status}`, `LEVEL: ${result.level}`, `CONTENT: ${result.content.status}`];
|
|
21
|
+
for (const error of result.errors ?? []) lines.push(`${error.code}: ${error.message}`);
|
|
22
|
+
return `${lines.join("\n")}\n`;
|
|
23
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { createCanonicalHandoff } from "../core/handoff.js";
|
|
2
|
+
|
|
3
|
+
export async function runHandoffCreate({ target, packageRoot, taskId, recipientHint, handoffNote } = {}) {
|
|
4
|
+
return createCanonicalHandoff(target, { packageRoot, taskId, recipientHint, note: handoffNote });
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function formatHandoffCreateResult(result) {
|
|
8
|
+
return `FORGELOOP HANDOFF CREATED\ntask: ${result.handoff.taskId}\nid: ${result.handoff.handoffId}\ndigest: ${result.handoff.artifactDigest}\npath: ${result.path}\n\n`;
|
|
9
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { listCanonicalHandoffs } from "../core/handoff.js";
|
|
2
|
+
|
|
3
|
+
export async function runHandoffList({ target, packageRoot, taskId } = {}) {
|
|
4
|
+
const handoffs = await listCanonicalHandoffs(target, { packageRoot, taskId });
|
|
5
|
+
return { taskId, count: handoffs.length, handoffs };
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function formatHandoffListResult(result) {
|
|
9
|
+
const lines = [`FORGELOOP HANDOFFS: ${result.count}`];
|
|
10
|
+
for (const handoff of result.handoffs) lines.push(`${handoff.handoffId} ${handoff.createdAt} ${handoff.artifactDigest}`);
|
|
11
|
+
return `${lines.join("\n")}\n`;
|
|
12
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { readCanonicalHandoff } from "../core/handoff.js";
|
|
2
|
+
|
|
3
|
+
export async function runHandoffShow({ target, packageRoot, taskId, handoffId } = {}) {
|
|
4
|
+
const artifact = await readCanonicalHandoff(target, { packageRoot, taskId, handoffId });
|
|
5
|
+
return { taskId, path: artifact.path, fingerprint: artifact.fingerprint, handoff: artifact.value };
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function formatHandoffShowResult(result) {
|
|
9
|
+
return `FORGELOOP HANDOFF: VALID\nid: ${result.handoff.handoffId}\ntask: ${result.handoff.taskId}\ndigest: ${result.handoff.artifactDigest}\npath: ${result.path}\n\n`;
|
|
10
|
+
}
|