@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.
Files changed (62) hide show
  1. package/.github/copilot-instructions.md +1 -1
  2. package/AGENTS.md +1 -1
  3. package/CLAUDE.md +1 -1
  4. package/CONTRIBUTING.md +90 -0
  5. package/DOCS_INDEX.md +13 -11
  6. package/ENG/c-development-eng.md +112 -0
  7. package/ENG/cpp-development-eng.md +109 -0
  8. package/ENG/dotnet-aspnetcore-development-eng.md +401 -0
  9. package/ENG/go-development-eng.md +103 -0
  10. package/ENG/java-development-eng.md +125 -0
  11. package/ENG/nodejs-backend-development-eng.md +605 -0
  12. package/ENG/php-development-eng.md +104 -0
  13. package/ENG/rust-development-eng.md +422 -0
  14. package/ENG/sql-development-eng.md +108 -0
  15. package/ENG/swift-development-eng.md +111 -0
  16. package/ENG/typescript-development-eng.md +108 -0
  17. package/GUIDE_ROUTER.md +418 -9
  18. package/QUALITY_SCORECARD.md +1 -0
  19. package/README.md +44 -33
  20. package/THIRD_PARTY_NOTICES.md +19 -7
  21. package/completions/_forgeloop +3 -3
  22. package/completions/forgeloop.bash +3 -3
  23. package/completions/forgeloop.fish +7 -0
  24. package/docs/AGENT_PROTOCOL_SUMMARY.md +55 -2
  25. package/docs/CLI_REFERENCE.md +28 -6
  26. package/docs/DOCUMENTATION_GUIDE.md +2 -1
  27. package/docs/GETTING_STARTED.md +59 -0
  28. package/docs/PACKAGE_CONTENTS.md +28 -14
  29. package/docs/RECIPES.md +23 -0
  30. package/docs/RELEASE_CHECKLIST.md +30 -2
  31. package/docs/TROUBLESHOOTING.md +100 -2
  32. package/docs/documentation-manifest.json +652 -0
  33. package/docs/protocol-requirements.json +77 -0
  34. package/package.json +19 -4
  35. package/schemas/routing-input.schema.json +1 -1
  36. package/scripts/CI_VALIDATORS.md +84 -11
  37. package/scripts/generate-agent-protocol-summary.mjs +36 -0
  38. package/src/commands/next.js +19 -7
  39. package/src/commands/task-create.js +84 -25
  40. package/src/commands/task-list.js +22 -2
  41. package/src/config/guides.json +44 -0
  42. package/src/core/build-script.js +151 -0
  43. package/src/core/c-cpp-project.js +143 -0
  44. package/src/core/cli-command-definitions.js +8 -1
  45. package/src/core/command-executors.js +5 -3
  46. package/src/core/command-input.js +140 -102
  47. package/src/core/contract-presets.js +82 -0
  48. package/src/core/error-codes.js +3 -3
  49. package/src/core/filesystem.js +1 -10
  50. package/src/core/go-project.js +206 -0
  51. package/src/core/java-project.js +403 -0
  52. package/src/core/multi-language-project.js +117 -0
  53. package/src/core/next-explanation.js +63 -0
  54. package/src/core/php-project.js +85 -0
  55. package/src/core/project-detection.js +1760 -52
  56. package/src/core/reconcile-closure.js +4 -1
  57. package/src/core/router.js +156 -3
  58. package/src/core/rust-project.js +400 -0
  59. package/src/core/sql-project.js +141 -0
  60. package/src/core/swift-project.js +200 -0
  61. package/src/core/typescript-project.js +349 -0
  62. 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.12.0",
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 node scripts/run-tests.js",
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 } }
@@ -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
- ## Node verification and receipt availability
44
+ ## Validation tiers
45
45
 
46
- The documentation workflow runs the documentation/tooling gates once on
47
- Linux with Node 24. Full suites cover Linux 20/22/24 and macOS/Windows 20/24,
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
- The historical required status `validate (22)` is retained as an aggregate
53
- gate over validation, the full portability matrix, and diagram checks. It
54
- fails if any prerequisite fails, is cancelled, or is skipped. This preserves
55
- the existing branch ruleset without running another duplicate test suite.
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 remains dependency-free.
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)}
@@ -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
- if (!compact) return result;
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: result.currentPhase,
28
+ phase: explained.currentPhase,
27
29
  profile,
28
- nextAction: result.nextAction,
29
- command: result.commandSpecs?.[0]?.argv ?? [],
30
- terminal: result.terminal,
31
- errors: result.reasonCodes ?? result.reasons?.map((reason) => reason.code) ?? [],
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
- export async function runTaskCreate({ target, packageRoot, taskId, claims = [], contractFile = null } = {}) {
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
- return withProjectClaimsLock(target, async () => {
73
- const allTasks = await discoverTasks(target, packageRoot);
74
- await assertNoScopeConflictsWithInspection(normalizedClaims, allTasks, taskId, { target, packageRoot });
75
- if (normalizedClaims.length > 0) {
76
- await assertScopeClean(target, normalizedClaims);
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 (contractFile) {
88
- const sourcePath = ensureWithin(target, contractFile);
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: tasks.map((task) => {
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
 
@@ -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
  }