@cassiomc1/forgeloop 0.1.0
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/.cursor/rules/project-loop.mdc +18 -0
- package/.forgeloop/.gitignore +2 -0
- package/.github/copilot-instructions.md +16 -0
- package/AGENTS.md +16 -0
- package/AGENT_COMPATIBILITY.md +147 -0
- package/CLAUDE.md +14 -0
- package/CONTRACT_COVERAGE.md +27 -0
- package/DELEGATION_PROTOCOL.md +91 -0
- package/ENG/accessibility-eng.md +155 -0
- package/ENG/clean-code-eng.md +223 -0
- package/ENG/design-code-eng.md +511 -0
- package/ENG/games-code-design-web-eng.md +751 -0
- package/ENG/perf-code-eng.md +441 -0
- package/ENG/premium-sites-studio-eng.md +320 -0
- package/ENG/sec-code-eng.md +706 -0
- package/ENG/test-code-eng.md +257 -0
- package/EXECUTION_STATE.md +107 -0
- package/GUIDE_ROUTER.md +274 -0
- package/LICENSE +21 -0
- package/LICENSE-DOCS.md +13 -0
- package/LOOP_ENGINEERING.md +551 -0
- package/LOOP_SYSTEM_DESIGN.md +394 -0
- package/ORCHESTRATOR_INTEGRATION.md +106 -0
- package/PROJECT_PROFILE.md +124 -0
- package/QUALITY_SCORECARD.md +54 -0
- package/README.md +492 -0
- package/TERMINOLOGY.md +21 -0
- package/THIRD_PARTY_NOTICES.md +129 -0
- package/THREAT_MODEL.md +35 -0
- package/package.json +51 -0
- package/schemas/delegated-result.schema.json +33 -0
- package/schemas/evidence.schema.json +15 -0
- package/schemas/execution-receipt.schema.json +46 -0
- package/schemas/routing-input.schema.json +17 -0
- package/schemas/routing-result.schema.json +17 -0
- package/schemas/task-brief.schema.json +24 -0
- package/schemas/work-state.schema.json +46 -0
- package/src/cli.js +341 -0
- package/src/commands/clear-state.js +11 -0
- package/src/commands/doctor.js +165 -0
- package/src/commands/init.js +42 -0
- package/src/commands/inspect.js +17 -0
- package/src/commands/route.js +32 -0
- package/src/commands/status.js +29 -0
- package/src/commands/update.js +109 -0
- package/src/commands/validate-protocol.js +133 -0
- package/src/commands/validate-receipt.js +19 -0
- package/src/commands/validate-state.js +30 -0
- package/src/core/agent-support.js +89 -0
- package/src/core/conformance.js +133 -0
- package/src/core/delegation.js +283 -0
- package/src/core/evidence.js +56 -0
- package/src/core/filesystem.js +122 -0
- package/src/core/inspect.js +115 -0
- package/src/core/json-safety.js +54 -0
- package/src/core/manifest.js +75 -0
- package/src/core/protocol.js +81 -0
- package/src/core/receipt.js +129 -0
- package/src/core/repository.js +19 -0
- package/src/core/router.js +296 -0
- package/src/core/schema-validation.js +179 -0
- package/src/core/templates.js +56 -0
- package/src/core/work-state.js +471 -0
package/THREAT_MODEL.md
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# ForgeLoop protocol threat model
|
|
2
|
+
|
|
3
|
+
**Status:** Implemented boundary inventory; verify target-specific permissions
|
|
4
|
+
and external publication evidence separately.
|
|
5
|
+
|
|
6
|
+
`ForgeLoop` reads untrusted project files and local protocol artifacts, but it is
|
|
7
|
+
not an agent runtime. It does not execute commands found in those files, call
|
|
8
|
+
an LLM, or publish on behalf of a target. The controls below describe the
|
|
9
|
+
remaining trust boundaries and their executable evidence.
|
|
10
|
+
|
|
11
|
+
| Threat | Impact | Trust boundary | Mitigation | Residual limitation | Test evidence |
|
|
12
|
+
| --- | --- | --- | --- | --- | --- |
|
|
13
|
+
| Path traversal | Writes or reads outside the selected target | Target path and every managed relative path | `ensureWithin`, safe-path checks, realpath containment, Windows-drive rejection | A separately privileged process can change the filesystem after validation | `tests/core.test.js`, `tests/portability.test.js`, `tests/fixtures/protocol/invalid/path-traversal.json` |
|
|
14
|
+
| Symlink escape | Redirects a managed path to another directory | Existing target parents and artifact paths | Reject symlinked targets, parents, and destinations before access | The check is not a filesystem lock | `tests/core.test.js`, `tests/portability.test.js`, `tests/fixtures/protocol/invalid/symlink-target.json` |
|
|
15
|
+
| Malicious manifest | Causes unsafe update or false ownership claims | `.forgeloop/manifest.json` | Schema, hash, safe-path, and preserve-flag validation; conflicts remain visible | A trusted operator can still deliberately edit the manifest | `tests/cli.test.js`, `tests/core.test.js` |
|
|
16
|
+
| Malicious work-state | Resumes stale, secret-bearing, or invalid work | `.forgeloop/work-state.json` | Schema/semantic checks, version checks, transition checks, contract/HEAD/artifact freshness, secret scan, size/depth bounds | Freshness cannot prove that an external process did not alter a file immediately afterward | `tests/work-state.test.js`, `tests/checkpoint-freshness.test.js`, `tests/security-limits.test.js` |
|
|
17
|
+
| 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` |
|
|
18
|
+
| 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` |
|
|
19
|
+
| 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` |
|
|
20
|
+
| Stale checkpoint replay | Repeats work against a changed contract or material file | Resume boundary between a saved state and current target | Contract fingerprint, repository fingerprint, required-artifact hashes, protocol version, and conservative revalidation | A contract author must provide the current contract file for comparison | `tests/checkpoint-freshness.test.js`, `tests/cli.test.js` |
|
|
21
|
+
| Unsafe publication claim | Local output is mistaken for push, PR, merge, or deployment | Receipt publication fields and evidence | Publication starts false; each true claim requires matching evidence; no CLI publication action | Remote provider state is outside local verification | `tests/receipt-semantics.test.js`, `README.md` |
|
|
22
|
+
| Schema confusion | A malformed or altered schema is treated as available | Shipped and target-local `schemas/` directory | Health checks parse every schema and classify valid, missing, invalid, or unsupported-version | Schema health does not replace review of semantic relationships | `tests/schema-health.test.js`, `src/core/schema-validation.js` |
|
|
23
|
+
| Protocol-version confusion | Old/future artifacts are silently reinterpreted | Cross-artifact protocol boundary | Explicit v1 checks and conformance errors for unsupported or mixed versions | A future protocol needs a deliberate migration implementation | `tests/conformance.test.js`, `tests/fixtures/protocol/invalid/` |
|
|
24
|
+
| Dependency-cycle denial of service | Coordination cannot make progress or spends unbounded work | Delegation dependency graph | Deterministic DFS cycle detection, unknown-reference rejection, and bounded JSON | A caller can still submit many valid tasks; host-level quotas remain external | `tests/delegation.test.js`, `tests/delegation-set.test.js` |
|
|
25
|
+
| Oversized JSON artifacts | Excess CPU or memory during validation | Any untrusted JSON artifact | Byte, depth, array, object-key, and string limits before semantic traversal | Limits are conservative defaults, not a complete resource scheduler | `tests/security-limits.test.js`, `src/core/json-safety.js` |
|
|
26
|
+
|
|
27
|
+
## Boundary rules
|
|
28
|
+
|
|
29
|
+
- Safe paths are checked before reading or writing; no protocol field is a
|
|
30
|
+
shell command.
|
|
31
|
+
- A `NOT_VERIFIED` or `BLOCKED` evidence record is never upgraded to observed
|
|
32
|
+
success by a formatter.
|
|
33
|
+
- Local checks do not imply remote publication, merge, deployment, or provider
|
|
34
|
+
confirmation.
|
|
35
|
+
- Credentials remain outside Git and outside protocol artifacts.
|
package/package.json
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@cassiomc1/forgeloop",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Portable, verifiable instruction kit for AI agents and developers",
|
|
5
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "https://github.com/cassiomc1/mdfiles.git"
|
|
8
|
+
},
|
|
9
|
+
"homepage": "https://github.com/cassiomc1/mdfiles#readme",
|
|
10
|
+
"license": "SEE LICENSE IN LICENSE",
|
|
11
|
+
"publishConfig": {
|
|
12
|
+
"access": "public"
|
|
13
|
+
},
|
|
14
|
+
"type": "module",
|
|
15
|
+
"engines": {
|
|
16
|
+
"node": ">=20"
|
|
17
|
+
},
|
|
18
|
+
"bin": {
|
|
19
|
+
"forgeloop": "src/cli.js"
|
|
20
|
+
},
|
|
21
|
+
"files": [
|
|
22
|
+
"src",
|
|
23
|
+
"ENG",
|
|
24
|
+
"schemas",
|
|
25
|
+
".forgeloop/.gitignore",
|
|
26
|
+
"AGENTS.md",
|
|
27
|
+
"CLAUDE.md",
|
|
28
|
+
"GUIDE_ROUTER.md",
|
|
29
|
+
"LOOP_ENGINEERING.md",
|
|
30
|
+
"LOOP_SYSTEM_DESIGN.md",
|
|
31
|
+
"QUALITY_SCORECARD.md",
|
|
32
|
+
"TERMINOLOGY.md",
|
|
33
|
+
"EXECUTION_STATE.md",
|
|
34
|
+
"DELEGATION_PROTOCOL.md",
|
|
35
|
+
"ORCHESTRATOR_INTEGRATION.md",
|
|
36
|
+
"THREAT_MODEL.md",
|
|
37
|
+
"CONTRACT_COVERAGE.md",
|
|
38
|
+
"AGENT_COMPATIBILITY.md",
|
|
39
|
+
"PROJECT_PROFILE.md",
|
|
40
|
+
"THIRD_PARTY_NOTICES.md",
|
|
41
|
+
"LICENSE",
|
|
42
|
+
"LICENSE-DOCS.md",
|
|
43
|
+
".cursor",
|
|
44
|
+
".github/copilot-instructions.md",
|
|
45
|
+
"README.md"
|
|
46
|
+
],
|
|
47
|
+
"scripts": {
|
|
48
|
+
"test": "node scripts/run-tests.js",
|
|
49
|
+
"pack:check": "node --test tests/package.test.js"
|
|
50
|
+
}
|
|
51
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "forgeloop://schemas/delegated-result.schema.json",
|
|
4
|
+
"title": "ForgeLoop delegated result",
|
|
5
|
+
"type": "object",
|
|
6
|
+
"required": ["schemaVersion", "protocolVersion", "taskId", "status", "changes", "verification", "openFindings", "limitations"],
|
|
7
|
+
"properties": {
|
|
8
|
+
"schemaVersion": { "const": 1 },
|
|
9
|
+
"protocolVersion": { "const": 1 },
|
|
10
|
+
"taskId": { "type": "string", "minLength": 1 },
|
|
11
|
+
"status": { "enum": ["complete", "blocked", "needs-context", "complete-with-concerns"] },
|
|
12
|
+
"changes": { "type": "array", "items": { "type": "string" } },
|
|
13
|
+
"verification": { "type": "array", "items": { "type": "string" } },
|
|
14
|
+
"evidence": {
|
|
15
|
+
"type": "array",
|
|
16
|
+
"items": {
|
|
17
|
+
"type": "object",
|
|
18
|
+
"required": ["kind", "source", "result"],
|
|
19
|
+
"properties": {
|
|
20
|
+
"schemaVersion": { "const": 1 },
|
|
21
|
+
"kind": { "enum": ["OBSERVED", "INFERRED", "NOT_VERIFIED", "BLOCKED"] },
|
|
22
|
+
"source": { "type": "string", "minLength": 1 },
|
|
23
|
+
"result": { "type": "string", "minLength": 1 },
|
|
24
|
+
"details": { "type": "object" }
|
|
25
|
+
},
|
|
26
|
+
"additionalProperties": false
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
"openFindings": { "type": "array", "items": { "type": "string" } },
|
|
30
|
+
"limitations": { "type": "array", "items": { "type": "string" } }
|
|
31
|
+
},
|
|
32
|
+
"additionalProperties": false
|
|
33
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "forgeloop://schemas/evidence.schema.json",
|
|
4
|
+
"title": "ForgeLoop evidence record",
|
|
5
|
+
"type": "object",
|
|
6
|
+
"required": ["schemaVersion", "kind", "source", "result"],
|
|
7
|
+
"properties": {
|
|
8
|
+
"schemaVersion": { "const": 1 },
|
|
9
|
+
"kind": { "enum": ["OBSERVED", "INFERRED", "NOT_VERIFIED", "BLOCKED"] },
|
|
10
|
+
"source": { "type": "string", "minLength": 1 },
|
|
11
|
+
"result": { "type": "string", "minLength": 1 },
|
|
12
|
+
"details": { "type": "object" }
|
|
13
|
+
},
|
|
14
|
+
"additionalProperties": false
|
|
15
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "forgeloop://schemas/execution-receipt.schema.json",
|
|
4
|
+
"title": "ForgeLoop execution receipt",
|
|
5
|
+
"type": "object",
|
|
6
|
+
"required": ["schemaVersion", "protocolVersion", "taskId", "contractFingerprint", "selectedGuides", "changedPaths", "checks", "review", "limitations", "publication"],
|
|
7
|
+
"properties": {
|
|
8
|
+
"schemaVersion": { "const": 1 },
|
|
9
|
+
"protocolVersion": { "const": 1 },
|
|
10
|
+
"taskId": { "type": "string", "minLength": 1 },
|
|
11
|
+
"contractFingerprint": { "type": "string", "pattern": "^[a-f0-9]{64}$" },
|
|
12
|
+
"status": { "enum": ["in-progress", "complete", "blocked", "complete-with-concerns"] },
|
|
13
|
+
"selectedGuides": { "type": "array", "items": { "type": "string" } },
|
|
14
|
+
"changedPaths": { "type": "array", "items": { "type": "string" } },
|
|
15
|
+
"checks": { "type": "array", "items": { "type": "object" } },
|
|
16
|
+
"evidence": {
|
|
17
|
+
"type": "array",
|
|
18
|
+
"items": {
|
|
19
|
+
"type": "object",
|
|
20
|
+
"required": ["kind", "source", "result"],
|
|
21
|
+
"properties": {
|
|
22
|
+
"schemaVersion": { "const": 1 },
|
|
23
|
+
"kind": { "enum": ["OBSERVED", "INFERRED", "NOT_VERIFIED", "BLOCKED"] },
|
|
24
|
+
"source": { "type": "string", "minLength": 1 },
|
|
25
|
+
"result": { "type": "string", "minLength": 1 },
|
|
26
|
+
"details": { "type": "object" }
|
|
27
|
+
},
|
|
28
|
+
"additionalProperties": false
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
"review": { "type": "object" },
|
|
32
|
+
"limitations": { "type": "array", "items": { "type": "string" } },
|
|
33
|
+
"publication": {
|
|
34
|
+
"type": "object",
|
|
35
|
+
"required": ["committed", "pushed", "pullRequest", "deployed"],
|
|
36
|
+
"properties": {
|
|
37
|
+
"committed": { "type": "boolean" },
|
|
38
|
+
"pushed": { "type": "boolean" },
|
|
39
|
+
"pullRequest": { "oneOf": [{ "type": "string" }, { "type": "null" }] },
|
|
40
|
+
"deployed": { "type": "boolean" }
|
|
41
|
+
},
|
|
42
|
+
"additionalProperties": false
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
"additionalProperties": false
|
|
46
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "forgeloop://schemas/routing-input.schema.json",
|
|
4
|
+
"title": "ForgeLoop routing input",
|
|
5
|
+
"type": "object",
|
|
6
|
+
"required": ["schemaVersion", "workType", "surfaces", "risks", "platforms"],
|
|
7
|
+
"properties": {
|
|
8
|
+
"schemaVersion": { "const": 1 },
|
|
9
|
+
"workType": { "type": "string", "minLength": 1 },
|
|
10
|
+
"surfaces": { "type": "array", "items": { "type": "string" } },
|
|
11
|
+
"risks": { "type": "array", "items": { "type": "string" } },
|
|
12
|
+
"platforms": { "type": "array", "items": { "type": "string" } },
|
|
13
|
+
"behaviorChange": { "type": "boolean" },
|
|
14
|
+
"executableChange": { "type": "boolean" }
|
|
15
|
+
},
|
|
16
|
+
"additionalProperties": false
|
|
17
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "forgeloop://schemas/routing-result.schema.json",
|
|
4
|
+
"title": "ForgeLoop routing result",
|
|
5
|
+
"type": "object",
|
|
6
|
+
"required": ["schemaVersion", "protocolVersion", "input", "primary", "guides", "reasons", "excluded"],
|
|
7
|
+
"properties": {
|
|
8
|
+
"schemaVersion": { "const": 1 },
|
|
9
|
+
"protocolVersion": { "const": 1 },
|
|
10
|
+
"input": { "type": "object" },
|
|
11
|
+
"primary": { "oneOf": [{ "type": "string" }, { "type": "null" }] },
|
|
12
|
+
"guides": { "type": "array", "items": { "type": "string" } },
|
|
13
|
+
"reasons": { "type": "object", "additionalProperties": { "type": "array", "items": { "type": "string" } } },
|
|
14
|
+
"excluded": { "type": "object", "additionalProperties": { "type": "array", "items": { "type": "string" } } }
|
|
15
|
+
},
|
|
16
|
+
"additionalProperties": false
|
|
17
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "forgeloop://schemas/task-brief.schema.json",
|
|
4
|
+
"title": "ForgeLoop delegation task brief",
|
|
5
|
+
"type": "object",
|
|
6
|
+
"required": ["schemaVersion", "protocolVersion", "taskId", "parentTaskId", "objective", "allowedPaths", "readOnlyPaths", "dependencies", "constraints", "requiredGuides", "verification", "authority", "deliverables"],
|
|
7
|
+
"properties": {
|
|
8
|
+
"schemaVersion": { "const": 1 },
|
|
9
|
+
"protocolVersion": { "const": 1 },
|
|
10
|
+
"taskId": { "type": "string", "minLength": 1 },
|
|
11
|
+
"parentTaskId": { "type": "string", "minLength": 1 },
|
|
12
|
+
"objective": { "type": "string", "minLength": 1 },
|
|
13
|
+
"allowedPaths": { "type": "array", "items": { "type": "string" } },
|
|
14
|
+
"readOnlyPaths": { "type": "array", "items": { "type": "string" } },
|
|
15
|
+
"dependencies": { "type": "array", "items": { "type": "string" } },
|
|
16
|
+
"constraints": { "type": "array", "items": { "type": "string" } },
|
|
17
|
+
"requiredGuides": { "type": "array", "items": { "type": "string" } },
|
|
18
|
+
"verification": { "type": "array", "items": { "type": "string" }, "minItems": 1 },
|
|
19
|
+
"authority": { "type": "array", "items": { "type": "string" } },
|
|
20
|
+
"deliverables": { "type": "array", "items": { "type": "string" }, "minItems": 1 },
|
|
21
|
+
"executionMode": { "enum": ["delegated", "inline"] }
|
|
22
|
+
},
|
|
23
|
+
"additionalProperties": false
|
|
24
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "forgeloop://schemas/work-state.schema.json",
|
|
4
|
+
"title": "ForgeLoop work state",
|
|
5
|
+
"type": "object",
|
|
6
|
+
"required": ["schemaVersion", "protocolVersion", "taskId", "contractFingerprint", "repositoryFingerprint", "phase", "selectedGuides", "completedSteps", "pendingSteps", "checks", "failures", "blockers", "lastUpdated"],
|
|
7
|
+
"properties": {
|
|
8
|
+
"schemaVersion": { "const": 1 },
|
|
9
|
+
"protocolVersion": { "const": 1 },
|
|
10
|
+
"taskId": { "type": "string", "minLength": 1 },
|
|
11
|
+
"contractFingerprint": { "type": "string", "pattern": "^[a-f0-9]{64}$" },
|
|
12
|
+
"repositoryFingerprint": {
|
|
13
|
+
"type": "object",
|
|
14
|
+
"required": ["branch", "head"],
|
|
15
|
+
"properties": {
|
|
16
|
+
"branch": { "oneOf": [{ "type": "string" }, { "type": "null" }] },
|
|
17
|
+
"head": { "oneOf": [{ "type": "string" }, { "type": "null" }] }
|
|
18
|
+
},
|
|
19
|
+
"additionalProperties": false
|
|
20
|
+
},
|
|
21
|
+
"phase": { "type": "string", "minLength": 1 },
|
|
22
|
+
"selectedGuides": { "type": "array", "items": { "type": "string" } },
|
|
23
|
+
"completedSteps": { "type": "array", "items": { "type": "string" } },
|
|
24
|
+
"pendingSteps": { "type": "array", "items": { "type": "string" } },
|
|
25
|
+
"requiredArtifacts": {
|
|
26
|
+
"type": "array",
|
|
27
|
+
"items": {
|
|
28
|
+
"type": "object",
|
|
29
|
+
"required": ["path", "sha256"],
|
|
30
|
+
"properties": {
|
|
31
|
+
"path": { "type": "string", "minLength": 1 },
|
|
32
|
+
"sha256": { "type": "string", "pattern": "^[a-f0-9]{64}$" }
|
|
33
|
+
},
|
|
34
|
+
"additionalProperties": false
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
"checks": { "type": "array", "items": { "type": "object" } },
|
|
38
|
+
"failures": { "type": "array", "items": { "type": "object" } },
|
|
39
|
+
"blockers": { "type": "array", "items": { "type": "object" } },
|
|
40
|
+
"lastUpdated": { "type": "string", "minLength": 1 },
|
|
41
|
+
"previousPhase": { "type": "string" },
|
|
42
|
+
"diagnosedHypothesis": { "type": "string", "minLength": 1 },
|
|
43
|
+
"verificationEvidence": { "type": "array", "items": { "type": "object" } }
|
|
44
|
+
},
|
|
45
|
+
"additionalProperties": false
|
|
46
|
+
}
|
package/src/cli.js
ADDED
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { realpathSync } from "node:fs";
|
|
3
|
+
import { readFile } from "node:fs/promises";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
|
|
7
|
+
import { runDoctor } from "./commands/doctor.js";
|
|
8
|
+
import { formatStatusResult, runStatus } from "./commands/status.js";
|
|
9
|
+
import { formatValidateStateResult, runValidateState } from "./commands/validate-state.js";
|
|
10
|
+
import { formatClearStateResult, runClearState } from "./commands/clear-state.js";
|
|
11
|
+
import { formatInspectResult, inspectTarget } from "./commands/inspect.js";
|
|
12
|
+
import { runInit } from "./commands/init.js";
|
|
13
|
+
import { formatRouteResult, runRoute } from "./commands/route.js";
|
|
14
|
+
import { runValidateReceipt } from "./commands/validate-receipt.js";
|
|
15
|
+
import { formatValidateProtocolResult, runValidateProtocol } from "./commands/validate-protocol.js";
|
|
16
|
+
import { runUpdate } from "./commands/update.js";
|
|
17
|
+
import { resolveTarget } from "./core/filesystem.js";
|
|
18
|
+
import { getPackageRoot } from "./core/templates.js";
|
|
19
|
+
|
|
20
|
+
function usage(command = null) {
|
|
21
|
+
const commands = "init|doctor|update|route|inspect|status|validate-state|clear-state|validate-receipt|validate-protocol";
|
|
22
|
+
const options = [" --path <directory> target project directory (default: current directory)"];
|
|
23
|
+
if (!command || command === "init" || command === "update") {
|
|
24
|
+
options.push(" --dry-run show planned writes without changing files");
|
|
25
|
+
}
|
|
26
|
+
if (!command || command === "doctor") {
|
|
27
|
+
options.push(" --json emit doctor findings as JSON");
|
|
28
|
+
options.push(" --strict treat warnings as unhealthy");
|
|
29
|
+
options.push(" --adopt <path> preserve an existing adapter in the manifest");
|
|
30
|
+
}
|
|
31
|
+
if (!command || command === "route") {
|
|
32
|
+
options.push(" --work <type> declared work type");
|
|
33
|
+
options.push(" --surface <value> affected surface (repeatable)");
|
|
34
|
+
options.push(" --risk <value> task risk (repeatable)");
|
|
35
|
+
options.push(" --platform <value> affected platform (repeatable)");
|
|
36
|
+
options.push(" --behavior-change declare behavior change");
|
|
37
|
+
options.push(" --executable-change declare executable/configuration change");
|
|
38
|
+
options.push(" --json emit route result as JSON");
|
|
39
|
+
}
|
|
40
|
+
if (!command || ["inspect", "status", "validate-state", "clear-state", "validate-receipt", "validate-protocol"].includes(command)) {
|
|
41
|
+
options.push(" --json emit structured output as JSON");
|
|
42
|
+
}
|
|
43
|
+
if (!command || ["status", "inspect", "validate-protocol"].includes(command)) {
|
|
44
|
+
options.push(" --contract-file <path> current JSON contract used for freshness comparison");
|
|
45
|
+
}
|
|
46
|
+
if (!command || command === "validate-protocol") {
|
|
47
|
+
options.push(" --route-file <path> routing-result JSON relative to target");
|
|
48
|
+
options.push(" --state-file <path> work-state JSON relative to target");
|
|
49
|
+
options.push(" --receipt-file <path> execution-receipt JSON relative to target");
|
|
50
|
+
options.push(" --task-brief-file <path> task brief JSON (repeatable)");
|
|
51
|
+
options.push(" --delegated-result-file <path> delegated result JSON (repeatable)");
|
|
52
|
+
}
|
|
53
|
+
if (!command || command === "validate-receipt") {
|
|
54
|
+
options.push(" --file <path> receipt file relative to target");
|
|
55
|
+
}
|
|
56
|
+
options.push(" --version show the installed package version");
|
|
57
|
+
options.push(" --help show this help");
|
|
58
|
+
|
|
59
|
+
return `Usage: forgeloop <${command ?? commands}> [options]\n\nOptions:\n${options.join("\n")}\n`;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function parseArgs(argv) {
|
|
63
|
+
const options = {
|
|
64
|
+
path: ".",
|
|
65
|
+
dryRun: false,
|
|
66
|
+
json: false,
|
|
67
|
+
strict: false,
|
|
68
|
+
adopt: [],
|
|
69
|
+
work: null,
|
|
70
|
+
surfaces: [],
|
|
71
|
+
risks: [],
|
|
72
|
+
platforms: [],
|
|
73
|
+
behaviorChange: false,
|
|
74
|
+
executableChange: false,
|
|
75
|
+
file: null,
|
|
76
|
+
contractFile: null,
|
|
77
|
+
routeFile: null,
|
|
78
|
+
stateFile: null,
|
|
79
|
+
receiptFile: null,
|
|
80
|
+
taskBriefFiles: [],
|
|
81
|
+
delegatedResultFiles: [],
|
|
82
|
+
help: false,
|
|
83
|
+
version: false,
|
|
84
|
+
};
|
|
85
|
+
let command = null;
|
|
86
|
+
|
|
87
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
88
|
+
const argument = argv[index];
|
|
89
|
+
if (["init", "doctor", "update", "route", "inspect", "status", "validate-state", "clear-state", "validate-receipt", "validate-protocol"].includes(argument)) {
|
|
90
|
+
if (command) throw new Error(`Multiple commands are not supported: ${argument}`);
|
|
91
|
+
command = argument;
|
|
92
|
+
} else if (argument === "--help" || argument === "-h") {
|
|
93
|
+
options.help = true;
|
|
94
|
+
} else if (argument === "--dry-run") {
|
|
95
|
+
options.dryRun = true;
|
|
96
|
+
} else if (argument === "--json") {
|
|
97
|
+
options.json = true;
|
|
98
|
+
} else if (argument === "--strict") {
|
|
99
|
+
options.strict = true;
|
|
100
|
+
} else if (argument === "--adopt") {
|
|
101
|
+
const relativePath = argv[index + 1];
|
|
102
|
+
if (!relativePath || relativePath.startsWith("-")) throw new Error("--adopt requires a path");
|
|
103
|
+
options.adopt.push(relativePath);
|
|
104
|
+
index += 1;
|
|
105
|
+
} else if (argument === "--work") {
|
|
106
|
+
const workType = argv[index + 1];
|
|
107
|
+
if (!workType || workType.startsWith("-")) throw new Error("--work requires a type");
|
|
108
|
+
options.work = workType;
|
|
109
|
+
index += 1;
|
|
110
|
+
} else if (argument === "--surface") {
|
|
111
|
+
const surface = argv[index + 1];
|
|
112
|
+
if (!surface || surface.startsWith("-")) throw new Error("--surface requires a value");
|
|
113
|
+
options.surfaces.push(surface);
|
|
114
|
+
index += 1;
|
|
115
|
+
} else if (argument === "--risk") {
|
|
116
|
+
const risk = argv[index + 1];
|
|
117
|
+
if (!risk || risk.startsWith("-")) throw new Error("--risk requires a value");
|
|
118
|
+
options.risks.push(risk);
|
|
119
|
+
index += 1;
|
|
120
|
+
} else if (argument === "--platform") {
|
|
121
|
+
const platform = argv[index + 1];
|
|
122
|
+
if (!platform || platform.startsWith("-")) throw new Error("--platform requires a value");
|
|
123
|
+
options.platforms.push(platform);
|
|
124
|
+
index += 1;
|
|
125
|
+
} else if (argument === "--behavior-change") {
|
|
126
|
+
options.behaviorChange = true;
|
|
127
|
+
} else if (argument === "--executable-change") {
|
|
128
|
+
options.executableChange = true;
|
|
129
|
+
} else if (argument === "--file") {
|
|
130
|
+
const file = argv[index + 1];
|
|
131
|
+
if (!file || file.startsWith("-")) throw new Error("--file requires a path");
|
|
132
|
+
options.file = file;
|
|
133
|
+
index += 1;
|
|
134
|
+
} else if (argument === "--contract-file") {
|
|
135
|
+
const contractFile = argv[index + 1];
|
|
136
|
+
if (!contractFile || contractFile.startsWith("-")) throw new Error("--contract-file requires a path");
|
|
137
|
+
options.contractFile = contractFile;
|
|
138
|
+
index += 1;
|
|
139
|
+
} else if (["--route-file", "--state-file", "--receipt-file"].includes(argument)) {
|
|
140
|
+
const file = argv[index + 1];
|
|
141
|
+
if (!file || file.startsWith("-")) throw new Error(`${argument} requires a path`);
|
|
142
|
+
if (argument === "--route-file") options.routeFile = file;
|
|
143
|
+
if (argument === "--state-file") options.stateFile = file;
|
|
144
|
+
if (argument === "--receipt-file") options.receiptFile = file;
|
|
145
|
+
index += 1;
|
|
146
|
+
} else if (argument === "--task-brief-file") {
|
|
147
|
+
const file = argv[index + 1];
|
|
148
|
+
if (!file || file.startsWith("-")) throw new Error("--task-brief-file requires a path");
|
|
149
|
+
options.taskBriefFiles.push(file);
|
|
150
|
+
index += 1;
|
|
151
|
+
} else if (argument === "--delegated-result-file") {
|
|
152
|
+
const file = argv[index + 1];
|
|
153
|
+
if (!file || file.startsWith("-")) throw new Error("--delegated-result-file requires a path");
|
|
154
|
+
options.delegatedResultFiles.push(file);
|
|
155
|
+
index += 1;
|
|
156
|
+
} else if (argument === "--path") {
|
|
157
|
+
options.path = argv[index + 1];
|
|
158
|
+
if (!options.path || options.path.startsWith("-")) throw new Error("--path requires a directory");
|
|
159
|
+
index += 1;
|
|
160
|
+
} else if (argument.startsWith("--path=")) {
|
|
161
|
+
options.path = argument.slice("--path=".length);
|
|
162
|
+
if (!options.path || options.path.startsWith("-")) throw new Error("--path requires a directory");
|
|
163
|
+
} else if (argument === "--version" || argument === "-v") {
|
|
164
|
+
options.version = true;
|
|
165
|
+
} else if (!argument.startsWith("-") && !command) {
|
|
166
|
+
command = argument;
|
|
167
|
+
} else {
|
|
168
|
+
throw new Error(`Unknown option: ${argument}`);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (!command) return { command: null, options };
|
|
173
|
+
|
|
174
|
+
const jsonCommands = ["doctor", "route", "inspect", "status", "validate-state", "clear-state", "validate-receipt", "validate-protocol"];
|
|
175
|
+
if (!jsonCommands.includes(command) && options.json) {
|
|
176
|
+
throw new Error(`Option --json is not valid for ${command}`);
|
|
177
|
+
}
|
|
178
|
+
if (![
|
|
179
|
+
"init",
|
|
180
|
+
"update",
|
|
181
|
+
].includes(command) && options.dryRun) {
|
|
182
|
+
throw new Error(`Option --dry-run is not valid for ${command}`);
|
|
183
|
+
}
|
|
184
|
+
if (command !== "doctor" && options.strict) {
|
|
185
|
+
throw new Error(`Option --strict is not valid for ${command}`);
|
|
186
|
+
}
|
|
187
|
+
if (command !== "doctor" && options.adopt.length > 0) {
|
|
188
|
+
throw new Error(`Option --adopt is not valid for ${command}`);
|
|
189
|
+
}
|
|
190
|
+
if (command !== "route" && (options.work || options.surfaces.length || options.risks.length || options.platforms.length || options.behaviorChange || options.executableChange)) {
|
|
191
|
+
throw new Error(`Route options are not valid for ${command}`);
|
|
192
|
+
}
|
|
193
|
+
if (command !== "validate-receipt" && options.file) {
|
|
194
|
+
throw new Error(`Option --file is not valid for ${command}`);
|
|
195
|
+
}
|
|
196
|
+
if (!["status", "inspect", "validate-protocol"].includes(command) && options.contractFile) {
|
|
197
|
+
throw new Error(`Option --contract-file is not valid for ${command}`);
|
|
198
|
+
}
|
|
199
|
+
if (command !== "validate-protocol" && (options.routeFile || options.stateFile || options.receiptFile || options.taskBriefFiles.length > 0 || options.delegatedResultFiles.length > 0)) {
|
|
200
|
+
throw new Error(`Protocol artifact options are not valid for ${command}`);
|
|
201
|
+
}
|
|
202
|
+
return { command, options };
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
async function packageVersion(packageRoot) {
|
|
206
|
+
const packageJson = JSON.parse(
|
|
207
|
+
await readFile(path.join(packageRoot, "package.json"), "utf8"),
|
|
208
|
+
);
|
|
209
|
+
return packageJson.version;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function printActions(actions) {
|
|
213
|
+
for (const item of actions) {
|
|
214
|
+
const reason = item.reason ? ` (${item.reason})` : "";
|
|
215
|
+
console.log(`${item.action.replaceAll("-", " ")}: ${item.path}${reason}`);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
export async function main(argv = process.argv.slice(2)) {
|
|
220
|
+
try {
|
|
221
|
+
const { command, options } = parseArgs(argv);
|
|
222
|
+
if (options.version) {
|
|
223
|
+
console.log(await packageVersion(getPackageRoot()));
|
|
224
|
+
return 0;
|
|
225
|
+
}
|
|
226
|
+
if (!command || options.help) {
|
|
227
|
+
console.log(usage(command));
|
|
228
|
+
return options.help ? 0 : 1;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const target = await resolveTarget(process.cwd(), options.path);
|
|
232
|
+
const packageRoot = getPackageRoot();
|
|
233
|
+
const version = await packageVersion(packageRoot);
|
|
234
|
+
|
|
235
|
+
if (command === "init") {
|
|
236
|
+
const result = await runInit({ target, dryRun: options.dryRun, packageRoot, packageVersion: version });
|
|
237
|
+
printActions(result.actions);
|
|
238
|
+
return 0;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
if (command === "doctor") {
|
|
242
|
+
const result = await runDoctor({
|
|
243
|
+
target,
|
|
244
|
+
packageRoot,
|
|
245
|
+
adoptPaths: options.adopt,
|
|
246
|
+
strict: options.strict,
|
|
247
|
+
});
|
|
248
|
+
if (options.json) {
|
|
249
|
+
console.log(JSON.stringify(result, null, 2));
|
|
250
|
+
} else {
|
|
251
|
+
for (const item of result.findings) {
|
|
252
|
+
console.log(`${item.severity}: ${item.code}: ${item.path} - ${item.message}`);
|
|
253
|
+
}
|
|
254
|
+
console.log(result.ok ? "healthy: ForgeLoop target is ready" : "unhealthy: ForgeLoop target needs attention");
|
|
255
|
+
}
|
|
256
|
+
return result.ok ? 0 : 1;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
if (command === "route") {
|
|
260
|
+
const result = runRoute({
|
|
261
|
+
workType: options.work,
|
|
262
|
+
surfaces: options.surfaces,
|
|
263
|
+
risks: options.risks,
|
|
264
|
+
platforms: options.platforms,
|
|
265
|
+
behaviorChange: options.behaviorChange,
|
|
266
|
+
executableChange: options.executableChange,
|
|
267
|
+
});
|
|
268
|
+
console.log(options.json ? JSON.stringify(result, null, 2) : formatRouteResult(result));
|
|
269
|
+
return 0;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
if (command === "inspect") {
|
|
273
|
+
const result = await inspectTarget({ target, packageRoot, contractFile: options.contractFile });
|
|
274
|
+
console.log(options.json ? JSON.stringify(result, null, 2) : formatInspectResult(result));
|
|
275
|
+
return result.ok ? 0 : 1;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
if (command === "validate-receipt") {
|
|
279
|
+
const result = await runValidateReceipt({ target, packageRoot, file: options.file });
|
|
280
|
+
console.log(options.json ? JSON.stringify(result, null, 2) : "valid: execution receipt");
|
|
281
|
+
return 0;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
if (command === "validate-protocol") {
|
|
285
|
+
const result = await runValidateProtocol({
|
|
286
|
+
target,
|
|
287
|
+
packageRoot,
|
|
288
|
+
routeFile: options.routeFile,
|
|
289
|
+
stateFile: options.stateFile,
|
|
290
|
+
receiptFile: options.receiptFile,
|
|
291
|
+
contractFile: options.contractFile,
|
|
292
|
+
taskBriefFiles: options.taskBriefFiles,
|
|
293
|
+
delegatedResultFiles: options.delegatedResultFiles,
|
|
294
|
+
});
|
|
295
|
+
console.log(options.json ? JSON.stringify(result, null, 2) : formatValidateProtocolResult(result));
|
|
296
|
+
return result.status === "VALID" ? 0 : 1;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
if (command === "status") {
|
|
300
|
+
const result = await runStatus({ target, packageRoot, contractFile: options.contractFile });
|
|
301
|
+
console.log(options.json ? JSON.stringify(result, null, 2) : formatStatusResult(result));
|
|
302
|
+
return 0;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
if (command === "validate-state") {
|
|
306
|
+
const result = await runValidateState({ target, packageRoot });
|
|
307
|
+
console.log(options.json ? JSON.stringify(result, null, 2) : formatValidateStateResult(result));
|
|
308
|
+
return result.ok ? 0 : 1;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
if (command === "clear-state") {
|
|
312
|
+
const result = await runClearState({ target });
|
|
313
|
+
console.log(options.json ? JSON.stringify(result, null, 2) : formatClearStateResult(result));
|
|
314
|
+
return 0;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
const result = await runUpdate({ target, dryRun: options.dryRun, packageRoot, packageVersion: version });
|
|
318
|
+
printActions(result.actions);
|
|
319
|
+
for (const conflict of result.conflicts) {
|
|
320
|
+
console.log(`conflict: ${conflict.path} - ${conflict.message}`);
|
|
321
|
+
}
|
|
322
|
+
return result.conflicts.length === 0 ? 0 : 1;
|
|
323
|
+
} catch (error) {
|
|
324
|
+
console.error(`error: ${error.message}`);
|
|
325
|
+
return 1;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
function isMainModule() {
|
|
330
|
+
if (!process.argv[1]) return false;
|
|
331
|
+
try {
|
|
332
|
+
return realpathSync(process.argv[1]) === realpathSync(fileURLToPath(import.meta.url));
|
|
333
|
+
} catch {
|
|
334
|
+
return false;
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
if (isMainModule()) {
|
|
339
|
+
const exitCode = await main();
|
|
340
|
+
process.exitCode = exitCode;
|
|
341
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { clearWorkState } from "../core/work-state.js";
|
|
2
|
+
|
|
3
|
+
export { clearWorkState };
|
|
4
|
+
|
|
5
|
+
export async function runClearState({ target }) {
|
|
6
|
+
return clearWorkState(target);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function formatClearStateResult(result) {
|
|
10
|
+
return `${result.removed ? "removed" : "absent"}: ${result.path}\n`;
|
|
11
|
+
}
|