@cassiomc1/forgeloop 1.12.0 → 1.13.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/.github/copilot-instructions.md +1 -1
- package/AGENTS.md +1 -1
- package/CLAUDE.md +1 -1
- package/CONTRIBUTING.md +90 -0
- package/DOCS_INDEX.md +13 -11
- package/ENG/c-development-eng.md +112 -0
- package/ENG/cpp-development-eng.md +109 -0
- package/ENG/dotnet-aspnetcore-development-eng.md +401 -0
- package/ENG/go-development-eng.md +103 -0
- package/ENG/java-development-eng.md +125 -0
- package/ENG/nodejs-backend-development-eng.md +605 -0
- package/ENG/php-development-eng.md +104 -0
- package/ENG/rust-development-eng.md +422 -0
- package/ENG/sql-development-eng.md +108 -0
- package/ENG/swift-development-eng.md +111 -0
- package/ENG/typescript-development-eng.md +108 -0
- package/GUIDE_ROUTER.md +418 -9
- package/QUALITY_SCORECARD.md +1 -0
- package/README.md +44 -33
- package/THIRD_PARTY_NOTICES.md +19 -7
- package/completions/_forgeloop +3 -3
- package/completions/forgeloop.bash +3 -3
- package/completions/forgeloop.fish +7 -0
- package/docs/AGENT_PROTOCOL_SUMMARY.md +55 -2
- package/docs/CLI_REFERENCE.md +28 -6
- package/docs/DOCUMENTATION_GUIDE.md +2 -1
- package/docs/GETTING_STARTED.md +59 -0
- package/docs/PACKAGE_CONTENTS.md +28 -14
- package/docs/RECIPES.md +23 -0
- package/docs/RELEASE_CHECKLIST.md +30 -2
- package/docs/TROUBLESHOOTING.md +100 -2
- package/docs/documentation-manifest.json +652 -0
- package/docs/protocol-requirements.json +77 -0
- package/package.json +19 -4
- package/schemas/routing-input.schema.json +1 -1
- package/scripts/CI_VALIDATORS.md +84 -11
- package/scripts/generate-agent-protocol-summary.mjs +36 -0
- package/src/commands/next.js +19 -7
- package/src/commands/task-create.js +84 -25
- package/src/commands/task-list.js +22 -2
- package/src/config/guides.json +44 -0
- package/src/core/build-script.js +151 -0
- package/src/core/c-cpp-project.js +143 -0
- package/src/core/cli-command-definitions.js +8 -1
- package/src/core/command-executors.js +5 -3
- package/src/core/command-input.js +140 -102
- package/src/core/contract-presets.js +82 -0
- package/src/core/error-codes.js +3 -3
- package/src/core/filesystem.js +1 -10
- package/src/core/go-project.js +206 -0
- package/src/core/java-project.js +403 -0
- package/src/core/multi-language-project.js +117 -0
- package/src/core/next-explanation.js +63 -0
- package/src/core/php-project.js +85 -0
- package/src/core/project-detection.js +1760 -52
- package/src/core/reconcile-closure.js +4 -1
- package/src/core/router.js +156 -3
- package/src/core/rust-project.js +400 -0
- package/src/core/sql-project.js +141 -0
- package/src/core/swift-project.js +200 -0
- package/src/core/typescript-project.js +349 -0
- package/src/core/xml-structure.js +123 -0
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 1,
|
|
3
|
+
"requirements": {
|
|
4
|
+
"FL-CONT-001": {
|
|
5
|
+
"source": "LOOP_ENGINEERING.md",
|
|
6
|
+
"statement": "A receiving harness MUST reconcile continuity against the current work state and checkout before acting on it.",
|
|
7
|
+
"tests": ["tests/continuity.test.js", "tests/documentation-conformance.test.js"],
|
|
8
|
+
"validator": "src/core/continuity.js"
|
|
9
|
+
},
|
|
10
|
+
"FL-CLAIM-001": {
|
|
11
|
+
"source": "PROTOCOL_INTEGRATION.md",
|
|
12
|
+
"statement": "Every harness MUST consume the canonical validated claim-state resolver.",
|
|
13
|
+
"tests": ["tests/task-claim-state.test.js", "tests/task-recovery-tamper.test.js"],
|
|
14
|
+
"validator": "src/core/task-claim-state.js"
|
|
15
|
+
},
|
|
16
|
+
"FL-CLAIM-002": {
|
|
17
|
+
"source": "LOOP_ENGINEERING.md",
|
|
18
|
+
"statement": "Readers without task-recovery schema v1 support MUST refuse claim ownership mutation.",
|
|
19
|
+
"tests": ["tests/protocol-info.test.js", "tests/documentation-conformance.test.js"],
|
|
20
|
+
"validator": "scripts/validate_documentation_conformance.mjs"
|
|
21
|
+
},
|
|
22
|
+
"FL-CLAIM-003": {
|
|
23
|
+
"source": "PROTOCOL_INTEGRATION.md",
|
|
24
|
+
"statement": "A reader without validated claim projection MUST fail closed.",
|
|
25
|
+
"tests": ["tests/protocol-info.test.js", "tests/package.test.js"],
|
|
26
|
+
"validator": "src/core/protocol-info.js"
|
|
27
|
+
},
|
|
28
|
+
"FL-WS-001": {
|
|
29
|
+
"source": "LOOP_ENGINEERING.md",
|
|
30
|
+
"statement": "A bound task MUST validate the current workspace before a task mutation or ForgeLoop-owned verification launch.",
|
|
31
|
+
"tests": ["tests/workspace-binding.test.js", "tests/workspace-binding-run-check.test.js"],
|
|
32
|
+
"validator": "src/core/workspace-binding.js"
|
|
33
|
+
},
|
|
34
|
+
"FL-HANDOFF-001": {
|
|
35
|
+
"source": "LOOP_ENGINEERING.md",
|
|
36
|
+
"statement": "A canonical handoff MUST remain an immutable protocol-derived snapshot.",
|
|
37
|
+
"tests": ["tests/handoff-envelope.test.js"],
|
|
38
|
+
"validator": "src/core/handoff.js"
|
|
39
|
+
},
|
|
40
|
+
"FL-SCOPE-001": {
|
|
41
|
+
"source": "LOOP_ENGINEERING.md",
|
|
42
|
+
"statement": "A verification scope MUST narrow execution only from current canonical evidence.",
|
|
43
|
+
"tests": ["tests/verification-scope.test.js", "tests/verification-scope-freshness.test.js"],
|
|
44
|
+
"validator": "src/core/verification-scope.js"
|
|
45
|
+
},
|
|
46
|
+
"FL-ATTEST-001": {
|
|
47
|
+
"source": "LOOP_ENGINEERING.md",
|
|
48
|
+
"statement": "A code attestation MUST bind exact source content to a valid completion receipt and event-ledger checkpoint.",
|
|
49
|
+
"tests": ["tests/code-manifest.test.js", "tests/attestation.test.js", "tests/attestation-verifier.test.js"],
|
|
50
|
+
"validator": "src/core/attestation-verifier.js"
|
|
51
|
+
},
|
|
52
|
+
"FL-ATTEST-002": {
|
|
53
|
+
"source": "PROTOCOL_INTEGRATION.md",
|
|
54
|
+
"statement": "An integration MUST consume the canonical attestation result and preserve its trust distinctions.",
|
|
55
|
+
"tests": ["tests/generic-ci-attestation.test.js", "tests/integration-runtime.test.js"],
|
|
56
|
+
"validator": "src/core/command-runtime.js"
|
|
57
|
+
},
|
|
58
|
+
"FL-ATTEST-003": {
|
|
59
|
+
"source": "PROTOCOL_INTEGRATION.md",
|
|
60
|
+
"statement": "A signing integration MUST keep private keys, OIDC tokens, and credentials outside ForgeLoop artifacts.",
|
|
61
|
+
"tests": ["tests/signing-provider.test.js"],
|
|
62
|
+
"validator": "src/core/signing/"
|
|
63
|
+
},
|
|
64
|
+
"FL-SQ-001": {
|
|
65
|
+
"source": "LOOP_ENGINEERING.md",
|
|
66
|
+
"statement": "Structural-quality evidence MUST remain provider-neutral, task-bound, delta-first, and separate from behavioral, security, performance, accessibility, and publication truth.",
|
|
67
|
+
"tests": ["tests/structural-quality-policy.test.js", "tests/structural-quality-lifecycle.test.js"],
|
|
68
|
+
"validator": "src/core/structural-quality/service.js"
|
|
69
|
+
},
|
|
70
|
+
"FL-SQ-002": {
|
|
71
|
+
"source": "PROTOCOL_INTEGRATION.md",
|
|
72
|
+
"statement": "A structural-quality integration MUST preserve unavailable, malformed, stale, incomparable, and blocked provider results without promoting them to PASS.",
|
|
73
|
+
"tests": ["tests/sentrux-structural-quality-provider.test.js", "tests/structural-quality-artifacts.test.js"],
|
|
74
|
+
"validator": "src/core/structural-quality/provider.js"
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cassiomc1/forgeloop",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.13.0",
|
|
4
4
|
"description": "Portable, verifiable engineering protocol for AI coding environments and developer workflows",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -51,6 +51,9 @@
|
|
|
51
51
|
".github/copilot-instructions.md",
|
|
52
52
|
"README.md",
|
|
53
53
|
"DOCS_INDEX.md",
|
|
54
|
+
"CONTRIBUTING.md",
|
|
55
|
+
"docs/documentation-manifest.json",
|
|
56
|
+
"docs/protocol-requirements.json",
|
|
54
57
|
"docs/diagrams",
|
|
55
58
|
"docs/assets/diagrams",
|
|
56
59
|
"docs/GETTING_STARTED.md",
|
|
@@ -103,6 +106,10 @@
|
|
|
103
106
|
],
|
|
104
107
|
"scripts": {
|
|
105
108
|
"test": "node scripts/run-tests.js",
|
|
109
|
+
"test:ci": "node scripts/run-tests.js --test-concurrency=2",
|
|
110
|
+
"test:watch": "node scripts/run-tests.js --watch",
|
|
111
|
+
"coverage:shard": "c8 --all --include=src/**/*.js --temp-directory=coverage-data npm run test:ci",
|
|
112
|
+
"coverage:report": "c8 --all --include=src/**/*.js --reporter=text --reporter=lcov --reporter=json-summary --temp-directory=coverage-data --check-coverage --lines=80 --functions=75 --branches=70 --statements=80 report",
|
|
106
113
|
"pack:check": "node --test tests/package.test.js",
|
|
107
114
|
"pack:smoke": "node scripts/package_smoke.mjs",
|
|
108
115
|
"release:identity": "node scripts/verify_release_identity.mjs",
|
|
@@ -116,12 +123,12 @@
|
|
|
116
123
|
"docs:report": "node scripts/report_documentation_health.mjs",
|
|
117
124
|
"docs:check": "node scripts/run-docs-check.js",
|
|
118
125
|
"lint": "eslint .",
|
|
119
|
-
"coverage": "c8 --all --include=src/**/*.js --reporter=text --reporter=lcov --reporter=json-summary --check-coverage --lines=80 --functions=75 --branches=70 --statements=80
|
|
126
|
+
"coverage": "c8 --all --include=src/**/*.js --reporter=text --reporter=lcov --reporter=json-summary --check-coverage --lines=80 --functions=75 --branches=70 --statements=80 npm run test:ci",
|
|
120
127
|
"dependency:policy": "node scripts/check-dependency-policy.mjs",
|
|
121
128
|
"mcp:test": "node scripts/run-mcp-tests.mjs",
|
|
122
129
|
"completions:generate": "node scripts/generate-shell-completions.mjs --write",
|
|
123
130
|
"completions:check": "node scripts/generate-shell-completions.mjs --check",
|
|
124
|
-
"test:quick": "node --test tests/cli.test.js tests/schema-health.test.js tests/schema-golden.test.js tests/protocol-info.test.js tests/integration-capabilities.test.js",
|
|
131
|
+
"test:quick": "node --test tests/cli.test.js tests/schema-health.test.js tests/schema-golden.test.js tests/protocol-info.test.js tests/integration-capabilities.test.js tests/ci-classify.test.js tests/validation-tiers.test.js",
|
|
125
132
|
"performance:check": "node scripts/benchmark-cli-startup.mjs",
|
|
126
133
|
"critical-coverage:check": "node scripts/check-critical-coverage.mjs",
|
|
127
134
|
"changelog:check": "node scripts/check-changelog-freshness.mjs",
|
|
@@ -140,7 +147,12 @@
|
|
|
140
147
|
"benchmark:profiles:regression": "node scripts/check-efficiency-regression.mjs",
|
|
141
148
|
"transactions:compact": "node scripts/compact-transactions.mjs",
|
|
142
149
|
"complexity:check": "node scripts/check-complexity.mjs",
|
|
143
|
-
"repository-index:manifest": "node scripts/verify-tgrep-manifest.mjs"
|
|
150
|
+
"repository-index:manifest": "node scripts/verify-tgrep-manifest.mjs",
|
|
151
|
+
"task-discovery:benchmark": "node scripts/benchmark-task-discovery.mjs",
|
|
152
|
+
"verify:fast": "node scripts/run-validation.mjs --tier fast",
|
|
153
|
+
"verify:local": "node scripts/run-validation.mjs --tier local",
|
|
154
|
+
"verify:prepush": "node scripts/run-validation.mjs --tier prepush",
|
|
155
|
+
"verify:release": "node scripts/run-validation.mjs --tier release"
|
|
144
156
|
},
|
|
145
157
|
"devDependencies": {
|
|
146
158
|
"c8": "^12.0.0",
|
|
@@ -148,6 +160,9 @@
|
|
|
148
160
|
"typescript": "7.0.2",
|
|
149
161
|
"yaml": "2.9.0"
|
|
150
162
|
},
|
|
163
|
+
"dependencies": {
|
|
164
|
+
"smol-toml": "1.8.0"
|
|
165
|
+
},
|
|
151
166
|
"exports": {
|
|
152
167
|
".": "./src/cli.js",
|
|
153
168
|
"./integration": {
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
"properties": {
|
|
19
19
|
"schemaVersion": { "const": 1 },
|
|
20
20
|
"scope": { "enum": ["MATCH", "UNSCOPED", "NO_MATCH", "NONE"] },
|
|
21
|
-
"frameworks": { "type": "array", "items": { "enum": ["flutter"] } },
|
|
21
|
+
"frameworks": { "type": "array", "items": { "enum": ["flutter", "dotnet", "aspnetcore", "abp", "nodejs", "rust", "c", "cpp", "java", "sql", "go", "typescript", "php", "swift"] } },
|
|
22
22
|
"projectRoots": { "type": "array", "items": { "type": "string", "minLength": 1 } },
|
|
23
23
|
"primarySignals": { "type": "array", "items": { "type": "string", "minLength": 1 } },
|
|
24
24
|
"supportingSignals": { "type": "array", "items": { "type": "string", "minLength": 1 } }
|
package/scripts/CI_VALIDATORS.md
CHANGED
|
@@ -41,26 +41,99 @@ Maintainers should distinguish between actual documentation link failures and ex
|
|
|
41
41
|
- **Link-content failure (`Lychee`)**: The link checking step runs with `--verbose` and outputs the exact failing URL along with the HTTP status code (e.g. 404, 403). If a documentation URL is broken, update the link in the source Markdown. If a legitimate external host blocks shared CI runners or aggressively rate-limits CI automation, add a minimal, targeted exclusion in `.lychee.toml` with an explanatory comment.
|
|
42
42
|
- **Action-download / Runner infrastructure failure (GitHub 429/502/503)**: When GitHub Actions fails during action checkout or tool download before running the test steps, this is a transient infrastructure issue rather than a project defect. Rerun the workflow without modifying project files.
|
|
43
43
|
|
|
44
|
-
##
|
|
44
|
+
## Validation tiers
|
|
45
45
|
|
|
46
|
-
The
|
|
47
|
-
|
|
48
|
-
with no repeated platform/version pair in that workflow. Node 24 Linux also
|
|
49
|
-
collects coverage. Package-content tests share one pack listing within their
|
|
50
|
-
process. The dedicated Windows main-branch workflow remains separate.
|
|
46
|
+
The local runner makes the validation boundary explicit and never installs a
|
|
47
|
+
missing tool implicitly:
|
|
51
48
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
49
|
+
| Tier | Command | Intended use |
|
|
50
|
+
| --- | --- | --- |
|
|
51
|
+
| Fast | `npm run verify:fast` | feedback while editing; quick Node, lint, dependency-policy, and generated-doc checks |
|
|
52
|
+
| Local | `npm run verify:local` | full deterministic source, documentation, manifest, and frozen Python checks |
|
|
53
|
+
| Pre-push | `npm run verify:prepush` | coverage, critical coverage, package/MCP checks, PoC checks, and the local suite expected before a PR |
|
|
54
|
+
| Release | `npm run verify:release` | pre-push checks plus package smoke, benchmark/profile checks, performance, release-identity prerequisites, and `npm pack --dry-run --json` |
|
|
55
|
+
|
|
56
|
+
Run `npm run mcp:setup` explicitly when the MCP package is not installed and
|
|
57
|
+
the MCP checks are in scope. If Python or another external validator is
|
|
58
|
+
unavailable, report `NOT_VERIFIED`; do not turn an unavailable check into a
|
|
59
|
+
pass by installing an unapproved tool.
|
|
60
|
+
|
|
61
|
+
The release tier reports `NOT_VERIFIED` until both
|
|
62
|
+
`FORGELOOP_RELEASE_VERSION` and `FORGELOOP_RELEASE_COMMIT` are supplied. When
|
|
63
|
+
present, those values are passed to the canonical `release:identity` command;
|
|
64
|
+
the runner never invents a release SHA or treats a pre-publication identity as
|
|
65
|
+
valid.
|
|
66
|
+
|
|
67
|
+
`npm run verify:prepush` deliberately runs coverage once and does not repeat
|
|
68
|
+
the full suite as a second `npm test`. `npm run lint`,
|
|
69
|
+
`npm run complexity:check`, and `npm run critical-coverage:check` retain
|
|
70
|
+
independent purposes: syntax and usage correctness, hotspot growth, and
|
|
71
|
+
coverage of critical modules. Packed TypeScript consumers validate the public
|
|
72
|
+
declarations; YAML-based tests validate workflow semantics. The core runtime
|
|
73
|
+
remains dependency-free.
|
|
74
|
+
|
|
75
|
+
## GitHub Actions boundary
|
|
76
|
+
|
|
77
|
+
Ordinary pull requests use `.github/workflows/pr-core.yml`. It preserves the
|
|
78
|
+
ruleset's exact required contexts:
|
|
79
|
+
|
|
80
|
+
- `audit`
|
|
81
|
+
- `CodeQL`
|
|
82
|
+
- `Verify generated Archify diagram`
|
|
83
|
+
- `validate (22)`
|
|
84
|
+
- `tarball smoke (ubuntu-latest)`
|
|
85
|
+
- `dependency-review`
|
|
86
|
+
|
|
87
|
+
`validate (22)` is an always-present, fail-closed aggregator over the core
|
|
88
|
+
matrix and the path-applicable documentation, audit, package, and native
|
|
89
|
+
Repository Index jobs. An optional Repository Index job may be skipped only
|
|
90
|
+
when the classifier says that the change cannot affect it; the aggregator
|
|
91
|
+
rejects every unexpected skip, failure, or cancellation. The aggregator is
|
|
92
|
+
the status required by the branch ruleset, not a second copy of the Node
|
|
93
|
+
suite.
|
|
94
|
+
|
|
95
|
+
The path classifier is deterministic and testable locally:
|
|
96
|
+
|
|
97
|
+
```bash
|
|
98
|
+
node scripts/ci-scenario-check.mjs
|
|
99
|
+
node scripts/ci-classify.mjs --paths README.md --json
|
|
100
|
+
node scripts/ci-classify.mjs --all --json
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
Documentation-only pull requests use quick core validation plus the
|
|
104
|
+
documentation/diagram path; the Node 20 matrix steps do not install or run for
|
|
105
|
+
docs-only changes. Runtime and package changes use Node 20 compatibility checks
|
|
106
|
+
and four Node 24 unit shards wrapped by the coverage gate, followed by a
|
|
107
|
+
coverage-only aggregation. Repository Index changes retain the native reusable
|
|
108
|
+
workflow and its platform matrix. Package
|
|
109
|
+
smoke is Ubuntu-only on ordinary PRs and expands to macOS/Windows through the
|
|
110
|
+
explicit release matrix. The main branch retains dedicated documentation,
|
|
111
|
+
Node compatibility, package smoke, audit, and Windows full-suite workflows;
|
|
112
|
+
those workflows are the place for broader post-merge or release validation.
|
|
113
|
+
|
|
114
|
+
CodeQL and dependency review remain independent GitHub security gates. The
|
|
115
|
+
repository's GitHub secret scanning and push protection remain enabled; the
|
|
116
|
+
custom `scan_secrets.py` check is a local/pre-push compatibility validator and
|
|
117
|
+
is not duplicated in every ordinary PR job.
|
|
118
|
+
|
|
119
|
+
## Receipts and infrastructure failures
|
|
56
120
|
|
|
57
121
|
`npm run lint`, `npm run complexity:check`, and
|
|
58
122
|
`npm run critical-coverage:check` retain independent purposes: syntax and
|
|
59
123
|
usage correctness, hotspot growth, and coverage of critical modules. Packed
|
|
60
124
|
TypeScript consumers validate the public declarations; YAML-based tests
|
|
61
|
-
validate workflow semantics. The core runtime
|
|
125
|
+
validate workflow semantics. The core runtime has the approved exact
|
|
126
|
+
`smol-toml` dependency for bounded Cargo manifest parsing; dependency policy
|
|
127
|
+
keeps all other runtime dependencies out.
|
|
62
128
|
|
|
63
129
|
`scripts/audit-receipts.mjs` runs after checkout. It audits supplied scoped
|
|
64
130
|
receipts with explicit task IDs, fails on an invalid audit, and reports
|
|
65
131
|
`NOT_VERIFIED` when none are supplied. This repository job does not create
|
|
66
132
|
lifecycle evidence from CI test results.
|
|
133
|
+
|
|
134
|
+
Maintainers should distinguish actual validation failures from external
|
|
135
|
+
CI/action infrastructure errors. A Lychee link failure identifies the exact
|
|
136
|
+
URL and status; update Markdown or add only a targeted, explained exclusion
|
|
137
|
+
for a legitimate shared-runner restriction. An action-download or runner
|
|
138
|
+
failure before project steps begin (for example GitHub 429/502/503) is
|
|
139
|
+
infrastructure and should be retried without changing project files.
|
|
@@ -7,6 +7,8 @@ import { fileURLToPath } from "node:url";
|
|
|
7
7
|
import { ARTIFACT_REGISTRY } from "../src/core/artifact-registry.js";
|
|
8
8
|
import { CLI_COMMAND_DEFINITIONS } from "../src/core/cli-command-definitions.js";
|
|
9
9
|
import { PUBLIC_ERROR_REGISTRY } from "../src/core/error-codes.js";
|
|
10
|
+
import { PROJECT_DETECTION_LIMITS, PROJECT_EVIDENCE_SCHEMA_VERSION } from "../src/core/project-detection.js";
|
|
11
|
+
import { GUIDE_REGISTRY } from "../src/core/guide-registry.js";
|
|
10
12
|
import { protocolInfo } from "../src/core/protocol-info.js";
|
|
11
13
|
|
|
12
14
|
const repositoryRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
@@ -43,6 +45,12 @@ function commandCatalog() {
|
|
|
43
45
|
.join("\n\n");
|
|
44
46
|
}
|
|
45
47
|
|
|
48
|
+
function guideCatalog() {
|
|
49
|
+
const rows = Object.entries(GUIDE_REGISTRY)
|
|
50
|
+
.map(([id, definition]) => [id, definition.path, definition.install ? "yes" : "no"]);
|
|
51
|
+
return `## Guide registry\n\n${table(["Guide", "Path", "Installable"], rows)}`;
|
|
52
|
+
}
|
|
53
|
+
|
|
46
54
|
async function render() {
|
|
47
55
|
const packageJson = JSON.parse(await readFile(path.join(repositoryRoot, "package.json"), "utf8"));
|
|
48
56
|
const info = protocolInfo({ packageVersion: packageJson.version });
|
|
@@ -95,6 +103,32 @@ Package version: ${packageJson.version}
|
|
|
95
103
|
6. Run forgeloop complete; accept completion only when the validator returns VALID.
|
|
96
104
|
7. Run forgeloop next again and follow the returned lifecycle action to a terminal state or an explicit blocker.
|
|
97
105
|
|
|
106
|
+
## Project evidence and guide routing
|
|
107
|
+
|
|
108
|
+
- The canonical guide registry is \`src/config/guides.json\`; the .NET
|
|
109
|
+
specialist has guide ID \`dotnet\` and resolves to
|
|
110
|
+
\`ENG/dotnet-aspnetcore-development-eng.md\`.
|
|
111
|
+
- Project evidence schema v${PROJECT_EVIDENCE_SCHEMA_VERSION} recognizes
|
|
112
|
+
structurally parsed Flutter, SDK-style .NET, Node.js, Rust, C, C++, Java,
|
|
113
|
+
SQL, Go, TypeScript, PHP, and Swift project evidence. SQL remains a bounded
|
|
114
|
+
owned-file overlay, while same-root language identities compose. ASP.NET Core
|
|
115
|
+
and ABP are conditional overlays recorded as reasons on \`dotnet\`; they
|
|
116
|
+
are not standalone guide IDs, and route validation requires each overlay to
|
|
117
|
+
include \`dotnet\`.
|
|
118
|
+
- Project detection is bounded by ${PROJECT_DETECTION_LIMITS.maxManifests}
|
|
119
|
+
manifests, ${PROJECT_DETECTION_LIMITS.maxSolutionFiles} solution files,
|
|
120
|
+
${PROJECT_DETECTION_LIMITS.maxManifestBytes} bytes per manifest,
|
|
121
|
+
${PROJECT_DETECTION_LIMITS.maxSourceFiles} supporting source files,
|
|
122
|
+
${PROJECT_DETECTION_LIMITS.maxSourceBytes} bytes per source file,
|
|
123
|
+
${PROJECT_DETECTION_LIMITS.maxVisitedDirectories} visited directories, and
|
|
124
|
+
${PROJECT_DETECTION_LIMITS.maxVisitedEntries} visited entries. It skips
|
|
125
|
+
symlinks and configured generated/vendor directories; exhausted budgets fail
|
|
126
|
+
closed rather than producing unbounded discovery.
|
|
127
|
+
- Task ownership discovery is a separate exhaustive operation. Task-list
|
|
128
|
+
filters and pagination project the validated discovery result and do not
|
|
129
|
+
remove ledger or recovery evidence. See \`GUIDE_ROUTER.md\` and
|
|
130
|
+
\`docs/CLI_REFERENCE.md\` for the operator-facing contracts.
|
|
131
|
+
|
|
98
132
|
## Adaptive execution profiles
|
|
99
133
|
|
|
100
134
|
\`complianceMode\` controls how strongly project policy is enforced. The
|
|
@@ -151,6 +185,8 @@ ${table(["Feature", "Version", "Supported"], features)}
|
|
|
151
185
|
|
|
152
186
|
${capabilityContracts}
|
|
153
187
|
|
|
188
|
+
${guideCatalog()}
|
|
189
|
+
|
|
154
190
|
## Public artifact registry
|
|
155
191
|
|
|
156
192
|
${table(["Key", "Scope", "Path", "Schema", "Trust role"], artifacts)}
|
package/src/commands/next.js
CHANGED
|
@@ -2,8 +2,9 @@ import { getNextAction } from "../core/next-action.js";
|
|
|
2
2
|
import { withResolvedTask } from "../core/task-command.js";
|
|
3
3
|
import { readPersistedRoute } from "../core/route-artifact.js";
|
|
4
4
|
import { projectExecutionProfile } from "../core/execution-profile.js";
|
|
5
|
+
import { explainNextAction } from "../core/next-explanation.js";
|
|
5
6
|
|
|
6
|
-
export async function runNext({ target, packageRoot, taskId, task, authorityContext, runtimeContext, compact = false }) {
|
|
7
|
+
export async function runNext({ target, packageRoot, taskId, task, authorityContext, runtimeContext, compact = false, explain = false }) {
|
|
7
8
|
return withResolvedTask(target, { taskId: taskId ?? task, packageRoot }, async (ctx) => {
|
|
8
9
|
const result = await getNextAction({
|
|
9
10
|
target,
|
|
@@ -12,7 +13,8 @@ export async function runNext({ target, packageRoot, taskId, task, authorityCont
|
|
|
12
13
|
authorityContext,
|
|
13
14
|
runtimeContext,
|
|
14
15
|
});
|
|
15
|
-
|
|
16
|
+
const explained = explain ? { ...result, explanation: explainNextAction(result) } : result;
|
|
17
|
+
if (!compact) return explained;
|
|
16
18
|
const compactTaskId = ctx?.taskId ?? (result.taskId && result.taskId !== "unknown" ? result.taskId : null);
|
|
17
19
|
let profile = null;
|
|
18
20
|
try {
|
|
@@ -23,12 +25,13 @@ export async function runNext({ target, packageRoot, taskId, task, authorityCont
|
|
|
23
25
|
}
|
|
24
26
|
return {
|
|
25
27
|
taskId: compactTaskId ?? result.taskId,
|
|
26
|
-
phase:
|
|
28
|
+
phase: explained.currentPhase,
|
|
27
29
|
profile,
|
|
28
|
-
nextAction:
|
|
29
|
-
command:
|
|
30
|
-
terminal:
|
|
31
|
-
errors:
|
|
30
|
+
nextAction: explained.nextAction,
|
|
31
|
+
command: explained.commandSpecs?.[0]?.argv ?? [],
|
|
32
|
+
terminal: explained.terminal,
|
|
33
|
+
errors: explained.reasonCodes ?? explained.reasons?.map((reason) => reason.code) ?? [],
|
|
34
|
+
...(explained.explanation ? { explanation: explained.explanation } : {}),
|
|
32
35
|
};
|
|
33
36
|
});
|
|
34
37
|
}
|
|
@@ -68,6 +71,15 @@ export function formatNextActionResult(result) {
|
|
|
68
71
|
lines.push(...result.missingArtifacts.map((artifact) => `- ${artifact}`));
|
|
69
72
|
}
|
|
70
73
|
if (result.terminal) lines.push("STATE: TERMINAL");
|
|
74
|
+
if (result.explanation) {
|
|
75
|
+
lines.push("EXPLANATION (BOUNDED, READ-ONLY):");
|
|
76
|
+
lines.push(`- ${result.explanation.summary}`);
|
|
77
|
+
for (const item of result.explanation.reasons) {
|
|
78
|
+
lines.push(`- ${item.code}: ${item.requiredChange}`);
|
|
79
|
+
if (item.safeArtifacts.length > 0) lines.push(` ARTIFACTS: ${item.safeArtifacts.join(", ")}`);
|
|
80
|
+
}
|
|
81
|
+
lines.push(`- ACTION KIND: ${result.explanation.actionKind}`);
|
|
82
|
+
}
|
|
71
83
|
return `${lines.join("\n")}\n`;
|
|
72
84
|
}
|
|
73
85
|
|
|
@@ -9,6 +9,7 @@ import { withTaskTransaction } from "../core/transaction.js";
|
|
|
9
9
|
import { taskDirectory } from "../core/task-paths.js";
|
|
10
10
|
import { ensureWithin, fileExists } from "../core/filesystem.js";
|
|
11
11
|
import { validateContract, writeContract } from "../core/contract.js";
|
|
12
|
+
import { createPresetContract, CONTRACT_PRESET_IDS } from "../core/contract-presets.js";
|
|
12
13
|
import { appendProtocolEvent } from "../core/events.js";
|
|
13
14
|
import { E_TASK_REQUIRED, E_TASK_ALREADY_EXISTS, E_TASK_DESCRIPTOR_INVALID } from "../core/error-codes.js";
|
|
14
15
|
import { recoveryGuidanceForClassification } from "../core/next-action-model.js";
|
|
@@ -56,12 +57,45 @@ export async function assertNoScopeConflictsWithInspection(claims, existingTasks
|
|
|
56
57
|
}
|
|
57
58
|
}
|
|
58
59
|
|
|
59
|
-
|
|
60
|
+
async function validateTaskCreateScope({ target, packageRoot, claims, taskId }) {
|
|
61
|
+
const allTasks = await discoverTasks(target, packageRoot);
|
|
62
|
+
await assertNoScopeConflictsWithInspection(claims, allTasks, taskId, { target, packageRoot });
|
|
63
|
+
if (claims.length > 0) await assertScopeClean(target, claims);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function previewResult({ taskId, normalizedClaims, preset, proposedContract }) {
|
|
67
|
+
return {
|
|
68
|
+
preview: true,
|
|
69
|
+
taskId,
|
|
70
|
+
writeClaims: normalizedClaims,
|
|
71
|
+
preset,
|
|
72
|
+
contract: proposedContract,
|
|
73
|
+
createsLifecycleState: false,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export async function runTaskCreate({
|
|
78
|
+
target,
|
|
79
|
+
packageRoot,
|
|
80
|
+
taskId,
|
|
81
|
+
claims = [],
|
|
82
|
+
contractFile = null,
|
|
83
|
+
preset = null,
|
|
84
|
+
preview = false,
|
|
85
|
+
} = {}) {
|
|
60
86
|
if (!taskId) {
|
|
61
87
|
throw taskError(E_TASK_REQUIRED, "--task is required for task-create");
|
|
62
88
|
}
|
|
63
89
|
assertTaskId(taskId);
|
|
64
90
|
|
|
91
|
+
if (preset && !CONTRACT_PRESET_IDS.includes(preset)) {
|
|
92
|
+
const error = new Error(`Unknown contract preset: ${preset}. Expected one of ${CONTRACT_PRESET_IDS.join(", ")}`);
|
|
93
|
+
error.code = "E_CONTRACT_PRESET_UNKNOWN";
|
|
94
|
+
throw error;
|
|
95
|
+
}
|
|
96
|
+
if (preset && contractFile) {
|
|
97
|
+
throw taskError("E_CONTRACT_INPUT_CONFLICT", "Use either --preset or --contract-file, not both");
|
|
98
|
+
}
|
|
65
99
|
const existing = await findTaskById(target, taskId, packageRoot);
|
|
66
100
|
if (existing) {
|
|
67
101
|
throw taskError(E_TASK_ALREADY_EXISTS, `Task already exists: ${taskId}`);
|
|
@@ -69,13 +103,45 @@ export async function runTaskCreate({ target, packageRoot, taskId, claims = [],
|
|
|
69
103
|
|
|
70
104
|
const normalizedClaims = normalizeWriteClaims(claims ?? []);
|
|
71
105
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
106
|
+
let proposedContract = null;
|
|
107
|
+
if (preset) proposedContract = createPresetContract({ taskId, preset, claims: normalizedClaims });
|
|
108
|
+
if (contractFile) {
|
|
109
|
+
const sourcePath = ensureWithin(target, contractFile);
|
|
110
|
+
if (!(await fileExists(sourcePath))) {
|
|
111
|
+
throw taskError("E_CONTRACT_MISSING", `Specified contract file not found: ${contractFile}`);
|
|
112
|
+
}
|
|
113
|
+
const raw = await readFile(sourcePath, "utf8");
|
|
114
|
+
try {
|
|
115
|
+
proposedContract = JSON.parse(raw);
|
|
116
|
+
} catch {
|
|
117
|
+
throw taskError("E_CONTRACT_INVALID", `Specified contract file is not valid JSON: ${contractFile}`);
|
|
77
118
|
}
|
|
119
|
+
await validateContract(proposedContract, packageRoot);
|
|
120
|
+
if (proposedContract.taskId && proposedContract.taskId !== taskId) {
|
|
121
|
+
throw taskError(
|
|
122
|
+
E_TASK_DESCRIPTOR_INVALID,
|
|
123
|
+
`Contract taskId "${proposedContract.taskId}" does not match requested taskId "${taskId}"`,
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
proposedContract = { ...proposedContract, taskId };
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (proposedContract) await validateContract({ ...proposedContract, taskId }, packageRoot);
|
|
130
|
+
|
|
131
|
+
// Preview is advisory: it must never create the claims-lock directory, remove
|
|
132
|
+
// a stale lock, or fail because another process currently owns the lock.
|
|
133
|
+
if (preview) {
|
|
134
|
+
await validateTaskCreateScope({ target, packageRoot, claims: normalizedClaims, taskId });
|
|
135
|
+
return previewResult({ taskId, normalizedClaims, preset, proposedContract });
|
|
136
|
+
}
|
|
78
137
|
|
|
138
|
+
return withProjectClaimsLock(target, async () => {
|
|
139
|
+
// Re-check after acquiring the reservation. Preview results are not a
|
|
140
|
+
// reservation and the repository may have changed since they were built.
|
|
141
|
+
if (await findTaskById(target, taskId, packageRoot)) {
|
|
142
|
+
throw taskError(E_TASK_ALREADY_EXISTS, `Task already exists: ${taskId}`);
|
|
143
|
+
}
|
|
144
|
+
await validateTaskCreateScope({ target, packageRoot, claims: normalizedClaims, taskId });
|
|
79
145
|
return withTaskTransaction({ target, taskId, operation: "task-create", packageRoot, recordCommitEvent: true }, async () => {
|
|
80
146
|
const descriptor = createTaskDescriptor({
|
|
81
147
|
taskId,
|
|
@@ -84,26 +150,9 @@ export async function runTaskCreate({ target, packageRoot, taskId, claims = [],
|
|
|
84
150
|
const written = await writeTaskDescriptor(target, descriptor, packageRoot);
|
|
85
151
|
|
|
86
152
|
let contractCopied = false;
|
|
87
|
-
if (
|
|
88
|
-
const
|
|
89
|
-
if (!(await fileExists(sourcePath))) {
|
|
90
|
-
throw taskError("E_CONTRACT_MISSING", `Specified contract file not found: ${contractFile}`);
|
|
91
|
-
}
|
|
92
|
-
const raw = await readFile(sourcePath, "utf8");
|
|
93
|
-
let parsed;
|
|
94
|
-
try {
|
|
95
|
-
parsed = JSON.parse(raw);
|
|
96
|
-
} catch {
|
|
97
|
-
throw taskError("E_CONTRACT_INVALID", `Specified contract file is not valid JSON: ${contractFile}`);
|
|
98
|
-
}
|
|
153
|
+
if (proposedContract) {
|
|
154
|
+
const parsed = { ...proposedContract, taskId };
|
|
99
155
|
await validateContract(parsed, packageRoot);
|
|
100
|
-
if (parsed.taskId && parsed.taskId !== taskId) {
|
|
101
|
-
throw taskError(
|
|
102
|
-
E_TASK_DESCRIPTOR_INVALID,
|
|
103
|
-
`Contract taskId "${parsed.taskId}" does not match requested taskId "${taskId}"`,
|
|
104
|
-
);
|
|
105
|
-
}
|
|
106
|
-
parsed.taskId = taskId;
|
|
107
156
|
await writeContract(target, parsed, packageRoot, { taskId });
|
|
108
157
|
contractCopied = true;
|
|
109
158
|
}
|
|
@@ -128,6 +177,16 @@ export async function runTaskCreate({ target, packageRoot, taskId, claims = [],
|
|
|
128
177
|
}
|
|
129
178
|
|
|
130
179
|
export function formatTaskCreateResult(result) {
|
|
180
|
+
if (result.preview) {
|
|
181
|
+
return `${JSON.stringify({
|
|
182
|
+
preview: true,
|
|
183
|
+
taskId: result.taskId,
|
|
184
|
+
preset: result.preset,
|
|
185
|
+
writeClaims: result.writeClaims,
|
|
186
|
+
contract: result.contract,
|
|
187
|
+
createsLifecycleState: false,
|
|
188
|
+
}, null, 2)}\n`;
|
|
189
|
+
}
|
|
131
190
|
const claims = result.writeClaims.length === 0 ? "none" : result.writeClaims.join(", ");
|
|
132
191
|
return `created task: ${result.taskId}\nkey: ${result.taskKey}\ndirectory: ${result.directory}\nclaims: ${claims}\n`;
|
|
133
192
|
}
|
|
@@ -1,9 +1,23 @@
|
|
|
1
1
|
import { discoverTasks } from "../core/task-discovery.js";
|
|
2
|
+
import { WORK_PHASES } from "../core/protocol.js";
|
|
2
3
|
|
|
3
|
-
export async function runTaskList({ target, packageRoot } = {}) {
|
|
4
|
+
export async function runTaskList({ target, packageRoot, phase = null, active = false, limit = null, offset = 0 } = {}) {
|
|
4
5
|
const tasks = await discoverTasks(target, packageRoot);
|
|
6
|
+
const normalizedPhase = typeof phase === "string" && phase.trim() !== "" ? phase.trim().toUpperCase() : null;
|
|
7
|
+
if (normalizedPhase && !WORK_PHASES.includes(normalizedPhase)) {
|
|
8
|
+
const error = new Error(`Unknown task phase filter: ${phase}`);
|
|
9
|
+
error.code = "E_TASK_PHASE_INVALID";
|
|
10
|
+
throw error;
|
|
11
|
+
}
|
|
12
|
+
const phaseFiltered = normalizedPhase ? tasks.filter((task) => task.phase === normalizedPhase) : tasks;
|
|
13
|
+
const filtered = active
|
|
14
|
+
? phaseFiltered.filter((task) => task.claimState === "ACTIVE" && task.mutationAllowed === true)
|
|
15
|
+
: phaseFiltered;
|
|
16
|
+
const start = Number.isInteger(offset) && offset >= 0 ? offset : 0;
|
|
17
|
+
const end = Number.isInteger(limit) && limit >= 0 ? start + limit : undefined;
|
|
18
|
+
const projected = filtered.slice(start, end);
|
|
5
19
|
return {
|
|
6
|
-
tasks:
|
|
20
|
+
tasks: projected.map((task) => {
|
|
7
21
|
if (task.healthy === false) {
|
|
8
22
|
return {
|
|
9
23
|
taskId: task.taskId ?? null,
|
|
@@ -35,6 +49,12 @@ export async function runTaskList({ target, packageRoot } = {}) {
|
|
|
35
49
|
updatedAt: task.updatedAt,
|
|
36
50
|
};
|
|
37
51
|
}),
|
|
52
|
+
...(normalizedPhase ? { phase: normalizedPhase } : {}),
|
|
53
|
+
...(active ? { active: true } : {}),
|
|
54
|
+
offset: start,
|
|
55
|
+
...(Number.isInteger(limit) && limit >= 0 ? { limit } : {}),
|
|
56
|
+
total: filtered.length,
|
|
57
|
+
hasMore: end !== undefined ? end < filtered.length : false,
|
|
38
58
|
};
|
|
39
59
|
}
|
|
40
60
|
|
package/src/config/guides.json
CHANGED
|
@@ -42,5 +42,49 @@
|
|
|
42
42
|
"flutter": {
|
|
43
43
|
"path": "ENG/flutter-development-eng.md",
|
|
44
44
|
"install": true
|
|
45
|
+
},
|
|
46
|
+
"dotnet": {
|
|
47
|
+
"path": "ENG/dotnet-aspnetcore-development-eng.md",
|
|
48
|
+
"install": true
|
|
49
|
+
},
|
|
50
|
+
"nodejs": {
|
|
51
|
+
"path": "ENG/nodejs-backend-development-eng.md",
|
|
52
|
+
"install": true
|
|
53
|
+
},
|
|
54
|
+
"rust": {
|
|
55
|
+
"path": "ENG/rust-development-eng.md",
|
|
56
|
+
"install": true
|
|
57
|
+
},
|
|
58
|
+
"c": {
|
|
59
|
+
"path": "ENG/c-development-eng.md",
|
|
60
|
+
"install": true
|
|
61
|
+
},
|
|
62
|
+
"cpp": {
|
|
63
|
+
"path": "ENG/cpp-development-eng.md",
|
|
64
|
+
"install": true
|
|
65
|
+
},
|
|
66
|
+
"java": {
|
|
67
|
+
"path": "ENG/java-development-eng.md",
|
|
68
|
+
"install": true
|
|
69
|
+
},
|
|
70
|
+
"sql": {
|
|
71
|
+
"path": "ENG/sql-development-eng.md",
|
|
72
|
+
"install": true
|
|
73
|
+
},
|
|
74
|
+
"go": {
|
|
75
|
+
"path": "ENG/go-development-eng.md",
|
|
76
|
+
"install": true
|
|
77
|
+
},
|
|
78
|
+
"typescript": {
|
|
79
|
+
"path": "ENG/typescript-development-eng.md",
|
|
80
|
+
"install": true
|
|
81
|
+
},
|
|
82
|
+
"php": {
|
|
83
|
+
"path": "ENG/php-development-eng.md",
|
|
84
|
+
"install": true
|
|
85
|
+
},
|
|
86
|
+
"swift": {
|
|
87
|
+
"path": "ENG/swift-development-eng.md",
|
|
88
|
+
"install": true
|
|
45
89
|
}
|
|
46
90
|
}
|