@cassiomc1/forgeloop 0.1.14 → 0.1.15

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.
@@ -1,2 +1,3 @@
1
1
  # Local resumable task state is untrusted, target-specific data.
2
2
  work-state.json
3
+ executions/
@@ -52,6 +52,7 @@ the supported ForgeLoop lifecycle commands or canonical ForgeLoop APIs:
52
52
  - `.forgeloop/work-state.json`
53
53
  - `.forgeloop/events.ndjson`
54
54
  - `.forgeloop/execution-receipt.json`
55
+ - `.forgeloop/executions/<executionId>.json`
55
56
  - completion recovery metadata
56
57
  - canonical check/evidence state
57
58
  - terminal-result lifecycle state
@@ -117,11 +118,32 @@ Every verification command path is classified by resolution mode:
117
118
  | `LOCAL_EXECUTABLE` | `node scripts/test.js`, `python3 -m unittest`, `./bin/check` | No | No |
118
119
  | `LOCAL_PACKAGE_BINARY` | `./node_modules/.bin/tool`, `npm test`, `pnpm test`, `yarn test` | No | No |
119
120
  | `NON_INSTALLING_RESOLUTION` | `npx --no-install tool`, `npx --no tool` | No | No |
120
- | `INSTALL_CAPABLE_RESOLUTION` | `npx tool`, `pnpm dlx tool`, `yarn dlx tool`, `bunx tool`, `uvx tool`, `pipx run tool` | Yes | Yes (`E_INSTALLATION_AUTHORITY_REQUIRED`) |
121
+ | `INSTALL_CAPABLE_RESOLUTION` | `npx tool`, `npm exec tool`, `npm x tool`, `pnpm dlx tool`, `yarn dlx tool`, `bunx tool`, `uvx tool`, `pipx run tool` | Yes | Yes (`E_INSTALLATION_AUTHORITY_REQUIRED`) |
121
122
  | `EXPLICIT_INSTALLATION` | `npm install tool`, `pnpm add tool`, `pip install tool`, `cargo install tool` | Yes | Yes (`E_INSTALLATION_AUTHORITY_REQUIRED`) |
122
123
 
123
124
  **Validator-enforced rule**: Any verification command executed via an installation-capable or explicit-installation resolution mode without a valid canonical installation authority grant is rejected by `record-check`, `audit`, and `complete` with error code `E_INSTALLATION_AUTHORITY_REQUIRED`, `E_AUTHORITY_INVALID`, `E_AUTHORITY_SCOPE_MISMATCH`, or `E_AUTHORITY_UNTRUSTED_SOURCE` and cannot contribute to `VALID` completion.
124
125
 
126
+ Recognized command dispatchers (such as `npm test`, `npm start`, `npm stop`, `npm restart`, `npm run <script>`, `npm run-script <script>`, `npm rum <script>`, `npm urn <script>`) are classified by their effective package resolution behavior across recognized lifecycle scripts before process launch. npm invocation parsing recognizes options (e.g. `--silent`, `--loglevel=error`) before the subcommand. Recognized npm-script dispatch is resolved recursively before process launch. `npm restart` uses npm's restart-specific lifecycle semantics (`prerestart`, `prestop`, `stop`, `poststop`, `prestart`, `start`, `poststart`, `postrestart` when `restart` is absent; `prerestart`, `restart`, `postrestart` when `restart` is present) rather than generic pre/main/post handling. ForgeLoop fails closed (`mayInstall: true`) when recursive npm-script resolution encounters a cycle or exceeds its maximum resolution depth (16). ForgeLoop does not resolve npm workspace selection in `run-check` for `0.1.15`. npm script executions using `--workspace`, `-w`, `--workspaces`, or `--ws` fail closed (`E_COMMAND_RESOLUTION_AMBIGUOUS`) because the effective `package.json` execution context may differ from the current ForgeLoop target. Run ForgeLoop against the selected workspace directory directly instead. If any nested lifecycle script invokes an installation-capable command (such as `npx`, `npm exec`, or `pnpm dlx`), the execution is elevated to `INSTALL_CAPABLE_RESOLUTION` and blocked before launch without authority.
127
+
128
+ **npm Classification Model**: npm classification is semantic and fail-closed. Unknown npm commands are not assumed safe. The classifier specifically identifies install-capable families including: `exec`/`x`, `install` aliases, `ci` aliases, `install-test` families, `install-ci-test` families, `update` aliases, `audit fix`, and conditional `init`/`create`/`innit` invocations. Unknown or ambiguous semantics fail closed (`E_COMMAND_RESOLUTION_AMBIGUOUS`).
129
+
130
+ Use `forgeloop run-check --id <id> --requirement <requirement> -- <argv>` for
131
+ observed command evidence. ForgeLoop preserves the exact argv vector, target
132
+ cwd, resolution classification, timestamps, exit status, and task/check
133
+ binding in `.forgeloop/executions/<executionId>.json` before recording the
134
+ check. Resolution is classified before process launch; install-capable
135
+ resolution is rejected without a valid host-attested authority, while
136
+ `npx --no-install` remains a non-installing path and may fail honestly when a
137
+ tool is absent. `run-check` launches the supplied argv without a shell.
138
+
139
+ `forgeloop record-check` is serialization-only. Its `--command` value is
140
+ metadata and is never executed. A `kind: command`, `evidenceKind: OBSERVED`
141
+ check must carry `provenance: FORGELOOP_EXECUTED` and a valid `executionRef`;
142
+ manual or actor-reported observations must use an explicit non-command kind or
143
+ provenance and must not be upgraded to command execution evidence. Completion,
144
+ audit, protocol validation, and bundles revalidate the referenced artifact and
145
+ its task, check, requirement, cycle, cwd, status, and exit-code binding.
146
+
125
147
  Authority cannot be self-issued by the actor consuming it. Boolean fields inside verification evidence (such as `installationAuthorized: true`) are not sufficient proof of installation authority. Installation authority must be established via a canonical authority grant supplied by a host/operator trust boundary and referenced via `installationAuthorityRef`.
126
148
 
127
149
  The runtime authority context has two modes:
@@ -509,7 +531,7 @@ prepare-completion
509
531
 
510
532
  run applicable project checks
511
533
 
512
- record observed results with record-check
534
+ run commands with run-check; record manual observations with record-check
513
535
  ↓ forgeloop next
514
536
  advance --to REVIEWING
515
537
  ↓ forgeloop next
@@ -524,10 +546,11 @@ subsequent `record-check` operations; completion remains invalid until required
524
546
  observed evidence, review state, chronology, and validator requirements are
525
547
  satisfied.
526
548
 
527
- The host agent runs applicable checks after the receipt exists, records their
528
- observed results with `record-check`, and queries `forgeloop next` before each
529
- subsequent lifecycle action. `record-check` records results already observed by
530
- the agent; it never executes the supplied command text.
549
+ The host agent runs applicable checks after the receipt exists, uses `run-check`
550
+ for commands, and uses `record-check` for manual or non-command observations.
551
+ `run-check` records exact command provenance; `record-check` records supplied
552
+ metadata only and never executes its `--command` value. Query `forgeloop next`
553
+ before each subsequent lifecycle action.
531
554
 
532
555
  Continue until the terminal outcome is either validator-backed `COMPLETE` or
533
556
  an explicitly reported `BLOCKED` / `PARTIALLY VERIFIED` result with exact
@@ -128,6 +128,7 @@ The following protocol artifacts are strictly owned by ForgeLoop:
128
128
  - `.forgeloop/work-state.json`
129
129
  - `.forgeloop/events.ndjson`
130
130
  - `.forgeloop/execution-receipt.json`
131
+ - `.forgeloop/executions/<executionId>.json`
131
132
  - Canonical check, evidence, and terminal-result state
132
133
 
133
134
  If the required CLI or API capability cannot be resolved:
@@ -140,6 +141,38 @@ If the required CLI or API capability cannot be resolved:
140
141
 
141
142
  Report the corresponding ForgeLoop dimension as `NOT_VERIFIED` / `E_FORGELOOP_CLI_UNAVAILABLE`.
142
143
 
144
+ ## Trusted command provenance
145
+
146
+ Command verification has two explicit paths:
147
+
148
+ - `forgeloop run-check --id <id> --requirement <requirement> -- <argv>` owns
149
+ execution. It classifies the exact argv before launch, uses a non-shell
150
+ process boundary, records the target cwd, resolution mode, timestamps,
151
+ exit status, and task/check binding in
152
+ `.forgeloop/executions/<executionId>.json`, then records an `OBSERVED` check
153
+ with `provenance: FORGELOOP_EXECUTED`.
154
+ - `forgeloop record-check` owns serialization only. `--command` is metadata and
155
+ is never launched. A `kind: command` check with `evidenceKind: OBSERVED`
156
+ requires both `executionRef` and `FORGELOOP_EXECUTED`; manual or actor-reported
157
+ observations use their explicit non-command/provenance values and remain
158
+ distinguishable from process execution.
159
+
160
+ `run-check` rejects install-capable resolution before process launch unless the
161
+ host supplies a valid trusted authority context. `npm exec` and `npm x` are
162
+ installation-capable resolution paths. Recognized command dispatchers (such as
163
+ `npm test`, `npm restart`, `npm run`, `npm rum`, `npm urn`) are resolved
164
+ recursively across recognized lifecycle scripts before launch with cycle
165
+ detection, leading option normalization, and restart-specific lifecycle fallback.
166
+ npm workspace script executions (`--workspace`, `-w`, `--workspaces`, `--ws`) fail
167
+ closed before launch with `E_COMMAND_RESOLUTION_AMBIGUOUS` because the effective
168
+ `package.json` context cannot be proven from the root target. Unknown npm command semantics are rejected before execution rather than downgraded to local execution. ForgeLoop uses an explicit non-installing allowlist instead of assuming that unrecognized npm commands are safe. `npx --no-install` is
169
+ an allowed non-installing resolution and can return a normal failed result when
170
+ the requested tool is unavailable. Completion, audit, `validate-protocol`, and task
171
+ bundles revalidate execution references rather than trusting duplicated check
172
+ metadata. Invalid or missing references return `E_EXECUTION_REF_INVALID`; an
173
+ observed command without ForgeLoop provenance returns
174
+ `E_COMMAND_PROVENANCE_UNATTESTED`.
175
+
143
176
  ## Missing tool capability
144
177
 
145
178
  A missing tool is a capability gap, not installation authority.
package/README.md CHANGED
@@ -110,11 +110,14 @@ owned cleanup. The frozen published installation under
110
110
  [`tests/fixtures/legacy-0.1.6/`](./tests/fixtures/legacy-0.1.6/) is derived
111
111
  from the real npm tarball, includes provenance and digests, and is copied into
112
112
  The latest verified published npm release is `@cassiomc1/forgeloop@0.1.14`.
113
- The repository release is `0.1.14`.
113
+ The repository package candidate is `@cassiomc1/forgeloop@0.1.15`; it is not
114
+ published yet.
114
115
  Earlier `0.1.8`, `0.1.9`, `0.1.10`, `0.1.11`, `0.1.12`, and `0.1.13` references are historical; never move
115
116
  their tags or `v0.1.10`. Release `0.1.14` enforces verification installation
116
117
  authority, provides recoverable stale receipt lifecycle in `prepare-completion`,
117
- and validates single-actor protocol runs.
118
+ and validates single-actor protocol runs. The `0.1.15` candidate adds trusted
119
+ command execution provenance without changing the v1 lifecycle or authority
120
+ boundary.
118
121
 
119
122
  ## How to prompt ForgeLoop
120
123
 
@@ -204,7 +207,8 @@ project without overwriting local instructions. When the package is available
204
207
  in the npm registry, use the commands below; otherwise use the repository
205
208
  checkout fallback.
206
209
 
207
- The current repository package is `@cassiomc1/forgeloop@0.1.14`.
210
+ The current repository package candidate is `@cassiomc1/forgeloop@0.1.15` and is
211
+ not yet published.
208
212
  The latest verified published npm release is `@cassiomc1/forgeloop@0.1.14`.
209
213
  For reproducible published-package runs or release-identity checks,
210
214
  pin the published version:
@@ -233,7 +237,8 @@ npx @cassiomc1/forgeloop advance --to PLANNED
233
237
  npx @cassiomc1/forgeloop advance --to EXECUTING
234
238
  npx @cassiomc1/forgeloop advance --to VERIFYING
235
239
  npx @cassiomc1/forgeloop prepare-completion --json
236
- npx @cassiomc1/forgeloop record-check --id tests --requirement tests --status passed --evidence-kind OBSERVED --command "npm test" --result "exit 0" --exit-code 0 --json
240
+ npx @cassiomc1/forgeloop run-check --json --id tests --requirement tests -- npm test
241
+ npx @cassiomc1/forgeloop record-check --id docs-review --kind manual-review --requirement "documentation synchronized" --status passed --evidence-kind OBSERVED --provenance MANUAL_OBSERVATION --result "reviewed" --json
237
242
  npx @cassiomc1/forgeloop record-terminal-result --requirement "Package published" --type PUBLICATION --status published --source "npm publish" --result "Published package to npm" --json
238
243
  npx @cassiomc1/forgeloop advance --to REVIEWING
239
244
  npx @cassiomc1/forgeloop audit --json
@@ -263,7 +268,7 @@ implementation
263
268
  → forgeloop next
264
269
  → prepare-completion
265
270
  → forgeloop next
266
- → checks + record-check
271
+ → checks + run-check/record-check
267
272
  → forgeloop next
268
273
  → advance --to REVIEWING
269
274
  → forgeloop next
@@ -287,10 +292,15 @@ Before implementation, write the canonical contract, persist the route, create
287
292
  required gate artifacts under `.forgeloop/gates/`, and require `preflight` to
288
293
  return `READY`. `advance` enforces legal phase transitions; it never runs the
289
294
  project's commands. After implementation, advance to `VERIFYING`, use
290
- `prepare-completion` to create a safe receipt skeleton, and use `record-check`
291
- to serialize results that the agent has already observed. `record-check` never
292
- executes the supplied command text. Advance to `REVIEWING` before running
293
- `audit` and `complete`.
295
+ `prepare-completion` to create a safe receipt skeleton, use `run-check` for
296
+ commands, and use `record-check` for manual or non-command observations.
297
+ `run-check` accepts the exact argv after `--`, classifies resolution before
298
+ launch, blocks install-capable resolution without trusted host authority, and
299
+ writes `.forgeloop/executions/<executionId>.json` before recording the check.
300
+ `record-check` never executes `--command`; that option is metadata only. A
301
+ command check with `OBSERVED` evidence must reference a ForgeLoop execution
302
+ artifact with `provenance: FORGELOOP_EXECUTED`. Advance to `REVIEWING` before
303
+ running `audit` and `complete`.
294
304
  `audit` is a read-only consistency check. `complete` validates the final
295
305
  contract, route, gates, phase ledger, structured evidence, coverage, receipt,
296
306
  and freshness before it can return `VALID`. `report` renders the same result as
@@ -377,6 +387,7 @@ target project. Its main threat boundaries are:
377
387
  | Installation authority provenance | Standalone CLI uses `trustMode: NONE`: environment-selected `FORGELOOP_AUTHORITY_FILE`/`FORGELOOP_AUTHORITY_DIR` sources are untrusted candidates; only an internal `HOST_ATTESTED` context may select a trusted source outside the actor-writable target. Project-local authority claims remain untrusted. |
378
388
  | Stale replay | Work state records contract and repository fingerprints; drift requires revalidation and never reruns destructive or publication actions automatically. |
379
389
  | Unverified publication | Receipts carry explicit publication booleans; local success never implies a push, pull request, merge, release, or deployment. |
390
+ | Unattested observed command | `record-check --command` is metadata only; command `OBSERVED` evidence requires a bound ForgeLoop execution artifact. `run-check` preserves exact argv, target cwd, resolution mode, timestamps, and exit status, and rejects install-capable resolution before launch without trusted host authority. |
380
391
 
381
392
  The full boundary inventory, residual limitations, and executable evidence are
382
393
  in [`THREAT_MODEL.md`](./THREAT_MODEL.md).
@@ -649,7 +660,7 @@ adoption. Local rendering requires Node.js 22+ and FFmpeg.
649
660
  The source repository keeps canonical documents at the root for package
650
661
  development and validation. A bootstrapped target uses the hidden-kit layout
651
662
  shown above; mutable contract, route, state, gate, event, preflight, and
652
- receipt artifacts remain directly under `.forgeloop/`.
663
+ receipt and execution artifacts remain directly under `.forgeloop/`.
653
664
 
654
665
  ## Maintenance
655
666
 
package/THREAT_MODEL.md CHANGED
@@ -20,6 +20,7 @@ remaining trust boundaries and their executable evidence.
20
20
  | Lifecycle artifact repair | Direct state or receipt edits fabricate a legal recovery or terminal phase | Work state, receipt, evidence checks, and event ledger | New verification cycles record phase events and fingerprints; validators reject state/ledger divergence and future lifecycle evidence | Local artifacts are detection-oriented, not cryptographically tamper-proof against a privileged process rewriting every linked artifact | `tests/lifecycle-evidence-recovery.test.js`, `tests/completion-ergonomics.test.js` |
21
21
  | Unsupported profile fact | Turns an agent decision into a durable user fact | `PROJECT_PROFILE.md` and `.forgeloop/sources.json` | Source IDs, source-kind validation, unknown-reference rejection, and explicit misclassification failures | Arbitrary Markdown semantics still require a human or host-specific parser | `tests/profile-provenance.test.js`, `src/core/profile.js` |
22
22
  | Weak verification | Treats a vague or inferred claim as observed evidence | Receipt checks and coverage | Versioned check schema, contradictory-status rejection, observed-evidence requirements, and coverage matrix | Evidence remains local and declarative; it is not a remote attestation service | `tests/evidence-coverage.test.js`, `tests/completion.test.js` |
23
+ | Unattested observed command | An actor supplies `--command "..."` or forged command metadata and makes an unrun process appear to be observed evidence | Check provenance, execution artifact, and process boundary | `run-check` captures exact argv, cwd, resolution, timestamps, exit status, and task/check binding; it classifies before launch, uses `shell: false`, and rejects install-capable resolution without trusted authority. `record-check --command` is metadata only; command `OBSERVED` checks require `FORGELOOP_EXECUTED` plus a valid `executionRef`; completion, audit, protocol validation, and bundles revalidate it | The local execution artifact is not cryptographic remote attestation, and a separately privileged process can alter the target after execution | `tests/run-check.test.js`, `tests/completion-cli.test.js`, `tests/verification-capability.test.js`, `tests/validate-protocol-cli.test.js` |
23
24
  | Malicious receipt | Turns local claims into false publication or completion claims | Execution receipt JSON | Semantic evidence checks for completion, checks, review, push, commit, and deployment; explicit publication booleans | Evidence text is declarative and must still be reviewed for provenance | `tests/receipt-semantics.test.js`, `tests/observability.test.js` |
24
25
  | Malicious task brief | Grants a child task more authority than intended | Delegation brief JSON and host harness | Relative path boundaries, guide/verification/authority checks, secret-free validation, set validation, parent integration ownership | The host harness remains responsible for OS permissions and execution policy | `tests/delegation.test.js`, `tests/delegation-set.test.js` |
25
26
  | Artifact content exposure | Sensitive material enters portable artifacts or diagnostics | State, receipt, delegation, evidence, and repository text | Nested key/value detection, shaped marker patterns, scanner coverage, no content echo in errors | Content scanners cannot prove that an unknown encoding is harmless | `tests/security-limits.test.js`, `tests/test_scan_secrets.py`, `scripts/scan_secrets.py` |
@@ -39,6 +40,10 @@ remaining trust boundaries and their executable evidence.
39
40
  | Forged local authority artifact | The active actor creates `.forgeloop/authorities/auth-modlens.json`, claims `source=operator`, and uses the reference to authorize its own installation-capable check | Host/operator authority source versus actor-writable project state | Resolve grants only from an explicit `HOST_ATTESTED` runtime context with an external source outside the target; reject project-local grants with `E_AUTHORITY_UNTRUSTED_SOURCE`; revalidate in `record-check`/`audit`/`complete` | If the host exposes the attested source as writable to the actor, ForgeLoop cannot distinguish genuine grants from actor fabrication without a stronger trust anchor | Local fake rejection, host-attested external acceptance, inside-target rejection, post-record removal and revocation tests |
40
41
  | Environment-injected trust root | The active actor creates an authority file outside the project target and sets `FORGELOOP_AUTHORITY_FILE` or `FORGELOOP_AUTHORITY_DIR` when invoking ForgeLoop | Actor-controlled process environment versus host-attested runtime authority | Standalone CLI uses `trustMode: NONE`; environment-selected sources are metadata/candidates only and fail with `E_AUTHORITY_UNTRUSTED_SOURCE`; trusted authority requires an internal `HOST_ATTESTED` context not exposed as a self-assertable CLI option | If the host integration allows the actor to mutate or replace the attested source/context, the host trust boundary is compromised | Environment-injection rejection, standalone audit/complete rejection, host-attested positive path |
41
42
  | Stale receipt recovery dead-end | Work state changes after preparing a receipt, leaving a mismatch that cannot be refreshed because the old receipt is rejected during re-preparation | Preparation lifecycle and recovery action resolution | Recoverable stale receipt binding in `prepareCompletion`, executable `PREPARE_COMPLETION` return from `next`, atomic refresh of stateFingerprint and changedPaths | Manual file corruption outside CLI commands requires manual diagnostic recovery | `tests/stale-receipt-recovery.test.js`, `tests/next-executability.test.js` |
43
+ | Recursive npm script dispatch | A recognized npm lifecycle script invokes another npm script, which later invokes an installation-capable resolver (e.g. `test` -> `npm run visual` -> `npx package`) | Recognized npm dispatcher semantics before ForgeLoop process launch | Recursive npm-script resolution with cycle detection, maximum depth (16), lifecycle hook inspection, restart special semantics, and fail-closed behavior when the resolver cannot prove the chain is non-installing | Opaque executables may spawn arbitrary descendants. Full descendant-process attestation requires host-level process controls and is outside this release | `tests/run-check.test.js`, `tests/verification-capability.test.js` |
44
+ | npm invocation rewriting and workspace dispatch | npm configuration flags appear before the subcommand, or npm workspace selectors cause script execution to occur against a package.json different from the ForgeLoop target root | Raw npm argv versus effective npm command and execution context | Canonical npm invocation parsing, effective subcommand extraction, workspace flag detection across the full npm argv, and fail-closed workspace script handling when the selected package.json cannot be proven from the current target | ForgeLoop 0.1.15 intentionally does not implement full npm workspace resolution. Users should execute run-check from the selected workspace target directory | `tests/run-check.test.js`, `tests/verification-capability.test.js` |
45
+ | Unclassified npm Install-Capable Command | The npm security classifier recognizes only a small denylist of package-mutating commands. Another official npm command or alias with install/update/bootstrap semantics falls through as a local package command | Effective npm command semantics versus ForgeLoop's command classifier | Semantic npm command classification with explicit install-capable families, explicit script-dispatch families, a deliberately small non-installing allowlist, and fail-closed behavior for unknown or ambiguous npm commands | Future npm commands are blocked until ForgeLoop explicitly classifies them | `tests/verification-capability.test.js`, `tests/run-check.test.js` |
46
+ | npm Option Value Ambiguity | An unknown npm config option with a separate value appears before the effective subcommand, and a parser misinterprets the option value as the npm subcommand | Raw npm argv and unsupported config syntax | Recognize self-contained `--key=value`, explicitly supported options-with-value, and known boolean options. Unknown long option followed by a non-option token fails closed with `NPM_OPTION_VALUE_AMBIGUOUS` | Unsupported npm config grammar is intentionally rejected rather than guessed | `tests/verification-capability.test.js`, `tests/run-check.test.js` |
42
47
 
43
48
  ## Boundary rules
44
49
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cassiomc1/forgeloop",
3
- "version": "0.1.14",
3
+ "version": "0.1.15",
4
4
  "description": "Portable, verifiable engineering protocol for AI coding environments and developer workflows",
5
5
  "repository": {
6
6
  "type": "git",
@@ -13,6 +13,8 @@
13
13
  "status": { "enum": ["passed", "failed", "blocked", "not-run"] },
14
14
  "evidenceKind": { "enum": ["OBSERVED", "INFERRED", "NOT_VERIFIED", "BLOCKED"] },
15
15
  "source": { "type": "string", "minLength": 1 },
16
+ "executionRef": { "type": "string", "minLength": 1 },
17
+ "provenance": { "enum": ["FORGELOOP_EXECUTED", "ACTOR_REPORTED", "MANUAL_OBSERVATION"] },
16
18
  "exitCode": { "type": "integer" },
17
19
  "timestamp": { "type": "string", "minLength": 1 },
18
20
  "repositoryFingerprint": { "type": "string", "pattern": "^[a-f0-9]{40}$" },
@@ -0,0 +1,63 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "forgeloop://schemas/execution.schema.json",
4
+ "title": "ForgeLoop command execution artifact",
5
+ "type": "object",
6
+ "required": [
7
+ "schemaVersion",
8
+ "protocolVersion",
9
+ "executionId",
10
+ "taskId",
11
+ "checkId",
12
+ "requirement",
13
+ "verificationCycle",
14
+ "kind",
15
+ "argv",
16
+ "cwd",
17
+ "resolution",
18
+ "startedAt",
19
+ "finishedAt",
20
+ "status",
21
+ "exitCode"
22
+ ],
23
+ "properties": {
24
+ "schemaVersion": { "const": 1 },
25
+ "protocolVersion": { "const": 1 },
26
+ "executionId": { "type": "string", "minLength": 1 },
27
+ "taskId": { "type": "string", "minLength": 1 },
28
+ "checkId": { "type": "string", "minLength": 1 },
29
+ "requirement": { "type": "string", "minLength": 1 },
30
+ "verificationCycle": { "type": "integer", "minimum": 1 },
31
+ "kind": { "const": "COMMAND_EXECUTION" },
32
+ "argv": {
33
+ "type": "array",
34
+ "minItems": 1,
35
+ "items": { "type": "string", "minLength": 1 }
36
+ },
37
+ "cwd": { "type": "string", "minLength": 1 },
38
+ "resolution": {
39
+ "type": "object",
40
+ "required": ["resolutionMode", "mayInstall", "installer", "tool"],
41
+ "properties": {
42
+ "resolutionMode": { "type": "string", "minLength": 1 },
43
+ "mayInstall": { "type": "boolean" },
44
+ "installer": { "oneOf": [{ "type": "string", "minLength": 1 }, { "type": "null" }] },
45
+ "tool": { "oneOf": [{ "type": "string", "minLength": 1 }, { "type": "null" }] }
46
+ },
47
+ "additionalProperties": false
48
+ },
49
+ "dispatch": {
50
+ "type": "object",
51
+ "properties": {
52
+ "kind": { "type": "string", "minLength": 1 },
53
+ "scriptName": { "type": "string", "minLength": 1 }
54
+ },
55
+ "additionalProperties": false
56
+ },
57
+ "startedAt": { "type": "string", "minLength": 1 },
58
+ "finishedAt": { "type": "string", "minLength": 1 },
59
+ "status": { "enum": ["passed", "failed"] },
60
+ "exitCode": { "oneOf": [{ "type": "integer", "minimum": 0 }, { "type": "null" }] }
61
+ },
62
+ "additionalProperties": false
63
+ }
package/src/cli.js CHANGED
@@ -24,6 +24,7 @@ import { formatPolicyResult, runPolicy } from "./commands/policy.js";
24
24
  import { formatBundleResult, runBundle } from "./commands/bundle.js";
25
25
  import { formatPrepareCompletionResult, runPrepareCompletion } from "./commands/prepare-completion.js";
26
26
  import { formatRecordCheckResult, runRecordCheck } from "./commands/record-check.js";
27
+ import { formatRunCheckResult, runCheck } from "./commands/run-check.js";
27
28
  import { formatRecordTerminalResult, runRecordTerminalResult } from "./commands/record-terminal-result.js";
28
29
  import { formatNextActionResult, runNext } from "./commands/next.js";
29
30
  import { resolveTarget } from "./core/filesystem.js";
@@ -31,7 +32,7 @@ import { getPackageRoot } from "./core/templates.js";
31
32
  import { ARTIFACT_PATHS } from "./core/artifacts.js";
32
33
 
33
34
  function usage(command = null) {
34
- const commands = "init|doctor|update|activate|route|preflight|advance|next|prepare-completion|record-check|record-terminal-result|complete|audit|report|policy|bundle|inspect|status|validate-state|clear-state|validate-receipt|validate-protocol";
35
+ const commands = "init|doctor|update|activate|route|preflight|advance|next|prepare-completion|run-check|record-check|record-terminal-result|complete|audit|report|policy|bundle|inspect|status|validate-state|clear-state|validate-receipt|validate-protocol";
35
36
  const options = [" --path <directory> target project directory (default: current directory)"];
36
37
  if (!command || command === "init" || command === "update") {
37
38
  options.push(" --dry-run show planned writes without changing files");
@@ -53,7 +54,7 @@ function usage(command = null) {
53
54
  if (!command || command === "advance") {
54
55
  options.push(" --to <phase> destination workflow phase");
55
56
  }
56
- if (!command || ["activate", "advance", "next", "prepare-completion", "record-check", "record-terminal-result", "preflight", "complete", "audit", "report", "policy", "bundle", "inspect", "status", "validate-state", "clear-state", "validate-receipt", "validate-protocol"].includes(command)) {
57
+ if (!command || ["activate", "advance", "next", "prepare-completion", "run-check", "record-check", "record-terminal-result", "preflight", "complete", "audit", "report", "policy", "bundle", "inspect", "status", "validate-state", "clear-state", "validate-receipt", "validate-protocol"].includes(command)) {
57
58
  options.push(" --json emit structured output as JSON");
58
59
  }
59
60
  if (!command || ["preflight", "complete", "audit", "report"].includes(command)) {
@@ -78,16 +79,23 @@ function usage(command = null) {
78
79
  if (!command || command === "validate-receipt") {
79
80
  options.push(" --file <path> receipt file relative to target");
80
81
  }
81
- if (!command || command === "record-check") {
82
+ if (!command || command === "record-check" || command === "run-check") {
82
83
  options.push(" --id <id> stable check identifier");
83
- options.push(" --kind <kind> check kind (default: command)");
84
84
  options.push(" --requirement <id> completion requirement covered by the check");
85
+ options.push(" --details <json> additional structured check details");
86
+ }
87
+ if (!command || command === "record-check") {
88
+ options.push(" --kind <kind> check kind (default: command; use manual-review for manual evidence)");
85
89
  options.push(" --status <status> passed, failed, blocked, or not-run");
86
90
  options.push(" --evidence-kind <kind> OBSERVED, INFERRED, NOT_VERIFIED, or BLOCKED");
87
- options.push(" --command <text> command already run by the agent (recorded only)");
88
- options.push(" --result <text> observed result supplied by the agent");
91
+ options.push(" --command <text> recorded only as metadata; it is never executed");
92
+ options.push(" --result <text> observed result supplied by the actor");
89
93
  options.push(" --exit-code <number> observed process exit code");
90
- options.push(" --details <json> additional structured check details");
94
+ options.push(" --execution-ref <id> ForgeLoop execution artifact reference");
95
+ options.push(" --provenance <value> FORGELOOP_EXECUTED, ACTOR_REPORTED, or MANUAL_OBSERVATION");
96
+ }
97
+ if (!command || command === "run-check") {
98
+ options.push(" -- <argv> exact command argv to classify, execute, and attest");
91
99
  }
92
100
  if (!command || command === "record-terminal-result") {
93
101
  options.push(" --requirement <id> terminal requirement covered by the result");
@@ -133,6 +141,9 @@ export function parseArgs(argv) {
133
141
  checkResult: null,
134
142
  checkExitCode: null,
135
143
  checkDetails: null,
144
+ checkExecutionRef: null,
145
+ checkProvenance: null,
146
+ commandArgv: [],
136
147
  checkType: null,
137
148
  checkSource: null,
138
149
  policy: null,
@@ -144,7 +155,7 @@ export function parseArgs(argv) {
144
155
 
145
156
  for (let index = 0; index < argv.length; index += 1) {
146
157
  const argument = argv[index];
147
- if (["init", "doctor", "update", "activate", "route", "preflight", "advance", "next", "prepare-completion", "record-check", "record-terminal-result", "complete", "audit", "report", "policy", "bundle", "inspect", "status", "validate-state", "clear-state", "validate-receipt", "validate-protocol"].includes(argument)) {
158
+ if (["init", "doctor", "update", "activate", "route", "preflight", "advance", "next", "prepare-completion", "run-check", "record-check", "record-terminal-result", "complete", "audit", "report", "policy", "bundle", "inspect", "status", "validate-state", "clear-state", "validate-receipt", "validate-protocol"].includes(argument)) {
148
159
  if (command) throw new Error(`Multiple commands are not supported: ${argument}`);
149
160
  command = argument;
150
161
  } else if (argument === "--help" || argument === "-h") {
@@ -304,6 +315,19 @@ export function parseArgs(argv) {
304
315
  throw new Error("--details must be a JSON object");
305
316
  }
306
317
  index += 1;
318
+ } else if (argument === "--execution-ref") {
319
+ const executionRef = argv[index + 1];
320
+ if (!executionRef || executionRef.startsWith("-")) throw new Error("--execution-ref requires an execution ID");
321
+ options.checkExecutionRef = executionRef;
322
+ index += 1;
323
+ } else if (argument === "--provenance") {
324
+ const provenance = argv[index + 1];
325
+ if (!provenance || provenance.startsWith("-")) throw new Error("--provenance requires a provenance value");
326
+ options.checkProvenance = provenance;
327
+ index += 1;
328
+ } else if (argument === "--" && command === "run-check") {
329
+ options.commandArgv = argv.slice(index + 1);
330
+ break;
307
331
  } else if (argument === "--path") {
308
332
  options.path = argv[index + 1];
309
333
  if (!options.path || options.path.startsWith("-")) throw new Error("--path requires a directory");
@@ -324,7 +348,7 @@ export function parseArgs(argv) {
324
348
 
325
349
  if (!command) return { command: null, options };
326
350
 
327
- const jsonCommands = ["doctor", "route", "activate", "advance", "next", "prepare-completion", "record-check", "record-terminal-result", "preflight", "complete", "audit", "report", "policy", "bundle", "inspect", "status", "validate-state", "clear-state", "validate-receipt", "validate-protocol"];
351
+ const jsonCommands = ["doctor", "route", "activate", "advance", "next", "prepare-completion", "run-check", "record-check", "record-terminal-result", "preflight", "complete", "audit", "report", "policy", "bundle", "inspect", "status", "validate-state", "clear-state", "validate-receipt", "validate-protocol"];
328
352
  if (!jsonCommands.includes(command) && options.json) {
329
353
  throw new Error(`Option --json is not valid for ${command}`);
330
354
  }
@@ -377,8 +401,10 @@ export function parseArgs(argv) {
377
401
  options.checkResult,
378
402
  options.checkExitCode,
379
403
  options.checkDetails,
404
+ options.checkExecutionRef,
405
+ options.checkProvenance,
380
406
  ];
381
- if (command !== "record-check" && checkOptions.some((value) => value !== null)) {
407
+ if (!["record-check", "run-check"].includes(command) && checkOptions.some((value) => value !== null)) {
382
408
  throw new Error(`Check recording options are not valid for ${command}`);
383
409
  }
384
410
  if (command === "record-check" && !options.help) {
@@ -388,6 +414,17 @@ export function parseArgs(argv) {
388
414
  if (!options.checkEvidenceKind) throw new Error("record-check requires --evidence-kind");
389
415
  if (!options.checkCommand && !options.checkResult) throw new Error("record-check requires --command or --result");
390
416
  }
417
+ if (command === "run-check" && !options.help) {
418
+ if (!options.checkId) throw new Error("run-check requires --id");
419
+ if (!options.checkRequirement) throw new Error("run-check requires --requirement");
420
+ if (options.checkKind || options.checkStatus || options.checkEvidenceKind || options.checkCommand
421
+ || options.checkResult || options.checkExitCode !== null || options.checkExecutionRef || options.checkProvenance) {
422
+ throw new Error("run-check accepts only --id, --requirement, --details, and -- <argv>");
423
+ }
424
+ if (!Array.isArray(options.commandArgv) || options.commandArgv.length === 0) {
425
+ throw new Error("run-check requires -- followed by an exact command argv");
426
+ }
427
+ }
391
428
  return { command, options };
392
429
  }
393
430
 
@@ -490,6 +527,19 @@ export async function main(argv = process.argv.slice(2)) {
490
527
  return 0;
491
528
  }
492
529
 
530
+ if (command === "run-check") {
531
+ const result = await runCheck({
532
+ target,
533
+ packageRoot,
534
+ id: options.checkId,
535
+ requirement: options.checkRequirement,
536
+ argv: options.commandArgv,
537
+ details: options.checkDetails ?? undefined,
538
+ });
539
+ console.log(options.json ? JSON.stringify(result, null, 2) : formatRunCheckResult(result));
540
+ return result.check.status === "passed" ? 0 : 1;
541
+ }
542
+
493
543
  if (command === "record-check") {
494
544
  const result = await runRecordCheck({
495
545
  target,
@@ -501,8 +551,10 @@ export async function main(argv = process.argv.slice(2)) {
501
551
  evidenceKind: options.checkEvidenceKind,
502
552
  command: options.checkCommand ?? undefined,
503
553
  result: options.checkResult ?? undefined,
504
- exitCode: options.checkExitCode,
554
+ ...(options.checkExitCode === null ? {} : { exitCode: options.checkExitCode }),
505
555
  details: options.checkDetails ?? undefined,
556
+ executionRef: options.checkExecutionRef ?? undefined,
557
+ provenance: options.checkProvenance ?? undefined,
506
558
  });
507
559
  console.log(options.json ? JSON.stringify(result, null, 2) : formatRecordCheckResult(result));
508
560
  return 0;
@@ -606,7 +658,7 @@ export async function main(argv = process.argv.slice(2)) {
606
658
  }
607
659
  return result.conflicts.length === 0 ? 0 : 1;
608
660
  } catch (error) {
609
- console.error(`error: ${error.message}`);
661
+ console.error(`error: ${error.code ? `${error.code}: ` : ""}${error.message}`);
610
662
  return 1;
611
663
  }
612
664
  }
@@ -0,0 +1,83 @@
1
+ import {
2
+ assertRecordCheckPrerequisites,
3
+ recordCheck as recordCheckArtifact,
4
+ } from "../core/completion-artifacts.js";
5
+ import { runCommandExecution } from "../core/execution.js";
6
+
7
+ export async function runCheck({
8
+ target,
9
+ packageRoot,
10
+ id,
11
+ requirement,
12
+ argv,
13
+ details,
14
+ authorityContext,
15
+ runtimeContext,
16
+ }) {
17
+ if (typeof id !== "string" || id.trim() === "" || typeof requirement !== "string" || requirement.trim() === "") {
18
+ const error = new Error("run-check requires non-empty id and requirement");
19
+ error.code = "E_CHECK_INVALID";
20
+ throw error;
21
+ }
22
+ const ready = await assertRecordCheckPrerequisites({
23
+ target,
24
+ packageRoot,
25
+ requirement,
26
+ status: "passed",
27
+ evidenceKind: "OBSERVED",
28
+ authorityContext,
29
+ runtimeContext,
30
+ });
31
+ const verificationCycle = ready.state.verificationCycle ?? 1;
32
+ const execution = await runCommandExecution({
33
+ target,
34
+ packageRoot,
35
+ taskId: ready.contract.value.taskId,
36
+ checkId: id,
37
+ requirement,
38
+ verificationCycle,
39
+ argv,
40
+ details,
41
+ authorityContext,
42
+ runtimeContext,
43
+ });
44
+ const status = execution.execution.status === "passed" ? "passed" : "failed";
45
+ const recorded = await recordCheckArtifact({
46
+ target,
47
+ packageRoot,
48
+ id,
49
+ kind: "command",
50
+ requirement,
51
+ status,
52
+ evidenceKind: "OBSERVED",
53
+ command: execution.execution.argv.join(" "),
54
+ result: execution.result,
55
+ ...(execution.execution.exitCode === null ? {} : { exitCode: execution.execution.exitCode }),
56
+ details,
57
+ executionRef: execution.execution.executionId,
58
+ provenance: "FORGELOOP_EXECUTED",
59
+ authorityContext,
60
+ runtimeContext,
61
+ });
62
+ return {
63
+ ...recorded,
64
+ execution: execution.execution,
65
+ executionPath: execution.path,
66
+ };
67
+ }
68
+
69
+ export function formatRunCheckResult(result) {
70
+ return [
71
+ "FORGELOOP CHECK EXECUTED",
72
+ `id: ${result.check.id}`,
73
+ `requirement: ${result.check.requirement}`,
74
+ `status: ${result.check.status}`,
75
+ `execution: ${result.execution.executionId}`,
76
+ `argv: ${result.execution.argv.join(" ")}`,
77
+ `exit code: ${result.execution.exitCode ?? "not-started"}`,
78
+ `artifact: ${result.executionPath}`,
79
+ `coverage: ${result.coverage.find((item) => item.requirement === result.check.requirement)?.status ?? "NOT_VERIFIED"}`,
80
+ `receipt: ${result.path}`,
81
+ "",
82
+ ].join("\n");
83
+ }
@@ -9,6 +9,7 @@ import { validateTaskBrief, validateDelegatedResult } from "../core/delegation.j
9
9
  import { ARTIFACT_PATHS, readJsonArtifact } from "../core/artifacts.js";
10
10
  import { evaluatePreflight, validateReadyProtocolConsistency } from "../core/preflight.js";
11
11
  import { validateEventLedger, validateStateLedgerCoherence } from "../core/events.js";
12
+ import { validateChecksExecutionProvenance } from "../core/completion-artifacts.js";
12
13
 
13
14
  async function readArtifact(target, relativePath, label) {
14
15
  if (!relativePath) return null;
@@ -87,6 +88,23 @@ export async function runValidateProtocol({
87
88
  await validateLoaded(loaded.find((item) => item.label === "route"), "routing-result", async (value) => assertRouteInvariants(value));
88
89
  await validateLoaded(loaded.find((item) => item.label === "state"), "work-state", async (value) => assertWorkStateSemantics(value));
89
90
  await validateLoaded(loaded.find((item) => item.label === "receipt"), "execution-receipt", async (value) => validateReceipt(value, packageRoot));
91
+ for (const [value, artifactPath] of [
92
+ [state, stateFile],
93
+ [receipt, receiptFile],
94
+ ]) {
95
+ if (!value) continue;
96
+ const provenanceErrors = await validateChecksExecutionProvenance(value.checks, {
97
+ target,
98
+ packageRoot,
99
+ taskId: value.taskId,
100
+ artifactPath,
101
+ });
102
+ schemaErrors.push(...provenanceErrors.map((error) => ({
103
+ ...error,
104
+ message: `Command provenance validation failed: ${error.message}`,
105
+ artifacts: [artifactPath, ...(error.artifacts ?? [])],
106
+ })));
107
+ }
90
108
  for (const item of loaded.filter((candidate) => candidate.label.startsWith("task brief:"))) {
91
109
  await validateLoaded(item, "task-brief", async (value) => validateTaskBrief(value, packageRoot));
92
110
  }
@@ -17,8 +17,16 @@ export const ARTIFACT_PATHS = Object.freeze({
17
17
  gates: ".forgeloop/gates",
18
18
  state: ".forgeloop/work-state.json",
19
19
  receipt: ".forgeloop/execution-receipt.json",
20
+ executionDirectory: ".forgeloop/executions",
20
21
  });
21
22
 
23
+ export function executionArtifactPath(executionId) {
24
+ if (typeof executionId !== "string" || !/^exec-[A-Za-z0-9_-]+$/.test(executionId)) {
25
+ throw new ArtifactError("E_EXECUTION_REF_INVALID", "Execution reference must be a simple execution ID");
26
+ }
27
+ return `${ARTIFACT_PATHS.executionDirectory}/${executionId}.json`;
28
+ }
29
+
22
30
  export class ArtifactError extends Error {
23
31
  constructor(code, message, artifacts = []) {
24
32
  super(message);