@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.
- package/.forgeloop/forgeloop.gitignore +1 -0
- package/LOOP_ENGINEERING.md +29 -6
- package/PROTOCOL_INTEGRATION.md +33 -0
- package/README.md +21 -10
- package/THREAT_MODEL.md +5 -0
- package/package.json +1 -1
- package/schemas/check.schema.json +2 -0
- package/schemas/execution.schema.json +63 -0
- package/src/cli.js +64 -12
- package/src/commands/run-check.js +83 -0
- package/src/commands/validate-protocol.js +18 -0
- package/src/core/artifacts.js +8 -0
- package/src/core/bundles.js +72 -0
- package/src/core/checks.js +16 -0
- package/src/core/completion-artifacts.js +229 -47
- package/src/core/completion.js +19 -1
- package/src/core/evidence-readiness.js +26 -1
- package/src/core/execution.js +185 -0
- package/src/core/schema-validation.js +1 -0
- package/src/core/templates.js +1 -0
- package/src/core/verification-capability.js +629 -18
package/LOOP_ENGINEERING.md
CHANGED
|
@@ -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
|
|
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,
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
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
|
package/PROTOCOL_INTEGRATION.md
CHANGED
|
@@ -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
|
|
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.
|
|
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
|
|
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,
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
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
|
@@ -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>
|
|
88
|
-
options.push(" --result <text> observed result supplied by the
|
|
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(" --
|
|
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 (
|
|
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
|
}
|
package/src/core/artifacts.js
CHANGED
|
@@ -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);
|