@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,82 @@
1
+ import { createContract } from "./contract.js";
2
+
3
+ const PRESET_DEFINITIONS = Object.freeze({
4
+ documentation: Object.freeze({
5
+ objective: "Complete the requested documentation change with verified project facts.",
6
+ constraints: ["Preserve repository-local instructions and existing public documentation contracts."],
7
+ risks: [],
8
+ verification: [{ id: "verify-documentation", text: "Relevant documentation checks pass.", type: "VERIFICATION" }],
9
+ successCriteria: [{ id: "complete-documentation", text: "The requested documentation change is complete and accurate.", type: "PRODUCT" }],
10
+ stopConditions: ["A required project fact or source cannot be confirmed."],
11
+ }),
12
+ bug: Object.freeze({
13
+ objective: "Diagnose and correct the requested bug without changing unrelated behavior.",
14
+ constraints: ["Preserve the existing public contract unless a breaking change is explicitly requested."],
15
+ risks: [],
16
+ verification: [{ id: "verify-bug-fix", text: "A focused regression check and proportional regression checks pass.", type: "VERIFICATION" }],
17
+ successCriteria: [{ id: "complete-bug-fix", text: "The reported behavior is corrected and the regression is covered.", type: "PRODUCT" }],
18
+ stopConditions: ["The failure cannot be reproduced or narrowed with available evidence."],
19
+ }),
20
+ feature: Object.freeze({
21
+ objective: "Implement the requested feature within the confirmed project scope.",
22
+ constraints: ["Preserve existing architecture and public contracts unless a change is explicitly requested."],
23
+ risks: [],
24
+ verification: [{ id: "verify-feature", text: "Focused behavior checks and proportional regression checks pass.", type: "VERIFICATION" }],
25
+ successCriteria: [{ id: "complete-feature", text: "The requested feature behavior is implemented and documented where applicable.", type: "PRODUCT" }],
26
+ stopConditions: ["Required product behavior or acceptance facts remain unresolved."],
27
+ }),
28
+ release: Object.freeze({
29
+ objective: "Prepare the requested release with independently verified artifact and publication evidence.",
30
+ constraints: [
31
+ "Do not publish, deploy, or mutate external release state without explicit authority.",
32
+ "Separate local checks, repository integration, publication, and deployment evidence.",
33
+ ],
34
+ risks: ["publication"],
35
+ verification: [{ id: "verify-release", text: "Release identity, package/artifact integrity, and required checks are independently verified.", type: "VERIFICATION" }],
36
+ successCriteria: [{
37
+ id: "publish-release",
38
+ text: "The requested release publication status is independently verified.",
39
+ type: "PUBLICATION",
40
+ requiredPublicationStatus: "published",
41
+ }],
42
+ stopConditions: ["Publication authority, target, or identity evidence is not available."],
43
+ }),
44
+ });
45
+
46
+ export const CONTRACT_PRESET_IDS = Object.freeze(Object.keys(PRESET_DEFINITIONS));
47
+
48
+ function normalizedClaims(claims) {
49
+ return [...new Set((Array.isArray(claims) ? claims : [])
50
+ .filter((claim) => typeof claim === "string" && claim.trim() !== "")
51
+ .map((claim) => claim.trim()))];
52
+ }
53
+
54
+ export function createPresetContract({ taskId, preset, claims = [] } = {}) {
55
+ if (!Object.prototype.hasOwnProperty.call(PRESET_DEFINITIONS, preset)) {
56
+ const error = new Error(`Unknown contract preset: ${preset}. Expected one of ${CONTRACT_PRESET_IDS.join(", ")}`);
57
+ error.code = "E_CONTRACT_PRESET_UNKNOWN";
58
+ throw error;
59
+ }
60
+ const definition = PRESET_DEFINITIONS[preset];
61
+ const confirmedClaims = normalizedClaims(claims);
62
+ const unresolvedDecisions = confirmedClaims.length > 0
63
+ ? []
64
+ : ["Confirm the concrete deliverables and affected behavior before implementation."];
65
+ return createContract({
66
+ taskId,
67
+ objective: definition.objective,
68
+ deliverables: confirmedClaims.length > 0 ? confirmedClaims : ["Deliverables confirmed by task scope."],
69
+ constraints: definition.constraints,
70
+ risks: definition.risks,
71
+ verification: definition.verification,
72
+ successCriteria: definition.successCriteria,
73
+ stopConditions: definition.stopConditions,
74
+ unresolvedDecisions,
75
+ sourceRefs: [`contract-preset:${preset}`],
76
+ });
77
+ }
78
+
79
+ export function contractPresetDefinition(preset) {
80
+ if (!Object.prototype.hasOwnProperty.call(PRESET_DEFINITIONS, preset)) return null;
81
+ return structuredClone(PRESET_DEFINITIONS[preset]);
82
+ }
@@ -788,8 +788,8 @@ export const PUBLIC_ERROR_CODES = Object.freeze({
788
788
  code: "E_RECONCILE_PHASE_INVALID",
789
789
  category: "lifecycle",
790
790
  classification: "PUBLIC_STABLE",
791
- meaning: "reconcile-closure was invoked for a task that is not EXECUTING or VERIFYING.",
792
- safeResolution: "reconcile-closure supports EXECUTING or VERIFYING tasks whose objective is already satisfied.",
791
+ meaning: "reconcile-closure was invoked for a task that is not EXECUTING, VERIFYING, or REVIEWING.",
792
+ safeResolution: "reconcile-closure supports EXECUTING, VERIFYING, or REVIEWING tasks whose objective is already satisfied.",
793
793
  }),
794
794
  E_RECONCILE_UNSUPPORTED_DRIFT: Object.freeze({
795
795
  code: "E_RECONCILE_UNSUPPORTED_DRIFT",
@@ -831,7 +831,7 @@ export const PUBLIC_ERROR_CODES = Object.freeze({
831
831
  category: "freshness",
832
832
  classification: "PUBLIC_STABLE",
833
833
  meaning: "The work-state checkpoint must be revalidated before the lifecycle can continue.",
834
- safeResolution: "Run forgeloop reconcile-closure for externally satisfied EXECUTING tasks, or inspect the freshness reasons for other drift.",
834
+ safeResolution: "Run forgeloop reconcile-closure for externally satisfied EXECUTING, VERIFYING, or REVIEWING tasks, or inspect the freshness reasons for other drift.",
835
835
  }),
836
836
  E_DIAGNOSIS_REQUIRED: Object.freeze({
837
837
  code: "E_DIAGNOSIS_REQUIRED",
@@ -129,7 +129,6 @@ export async function assertSafePath(root, relativePath) {
129
129
  }
130
130
 
131
131
  let existing = destination;
132
- let relativeExisting = path.normalize(relativePath);
133
132
  while (true) {
134
133
  try {
135
134
  const info = await lstatWithTransientWindowsRetry(existing);
@@ -137,14 +136,7 @@ export async function assertSafePath(root, relativePath) {
137
136
  throw new Error(`Path uses a symlink inside target directory: ${relativePath}`);
138
137
  }
139
138
  const resolvedRoot = await realpathWithTransientWindowsRetry(absoluteRoot);
140
- // Resolve the comparison path from the canonical root. Windows can
141
- // return different equivalent spellings (for example, short names or
142
- // extended-length prefixes) for the root and a child path when those
143
- // paths are canonicalized independently. Reconstructing the child from
144
- // the canonical root keeps the comparison in one namespace without
145
- // weakening the realpath containment check for junctions or symlinks.
146
- const canonicalExisting = path.resolve(resolvedRoot, relativeExisting);
147
- const resolvedExisting = await realpathWithTransientWindowsRetry(canonicalExisting);
139
+ const resolvedExisting = await realpathWithTransientWindowsRetry(existing);
148
140
  if (!isPathWithin(resolvedRoot, resolvedExisting)) {
149
141
  throw new Error(`Path escapes target directory: ${relativePath}`);
150
142
  }
@@ -154,7 +146,6 @@ export async function assertSafePath(root, relativePath) {
154
146
  const parent = path.dirname(existing);
155
147
  if (parent === existing) throw new Error(`Path does not resolve inside target directory: ${relativePath}`);
156
148
  existing = parent;
157
- relativeExisting = path.dirname(relativeExisting);
158
149
  }
159
150
  }
160
151
  }
@@ -0,0 +1,206 @@
1
+ import path from "node:path";
2
+
3
+ import { createLanguageProject, portablePath } from "./multi-language-project.js";
4
+
5
+ const GO_DIRECTIVE = /^(module|go|toolchain|godebug|require|replace|exclude|retract|tool|ignore)\b/u;
6
+ const GO_WORK_DIRECTIVES = new Set(["go", "toolchain", "godebug", "use", "replace", "tool"]);
7
+
8
+ function invalidGoResult() {
9
+ return {
10
+ valid: false,
11
+ module: null,
12
+ goVersion: null,
13
+ toolchain: null,
14
+ uses: [],
15
+ localReplacements: [],
16
+ ignorePaths: [],
17
+ };
18
+ }
19
+
20
+ function stripGoComment(line) {
21
+ const index = line.indexOf("//");
22
+ return (index < 0 ? line : line.slice(0, index)).trim();
23
+ }
24
+
25
+ function parseGoDirective(line) {
26
+ const block = line.match(/^(module|go|toolchain|godebug|require|replace|exclude|retract|use|tool|ignore)\b\s*\(\s*$/u);
27
+ const match = line.match(/^(module|go|toolchain|godebug|require|replace|exclude|retract|use|tool|ignore)\b\s*(?:\(([^)]*)\)|(.+))$/u);
28
+ if (block) return { name: block[1], value: "" };
29
+ if (!match) return null;
30
+ return {
31
+ name: match[1],
32
+ value: (match[2] ?? match[3] ?? "").trim(),
33
+ };
34
+ }
35
+
36
+ function collectGoBlock(lines, start) {
37
+ const values = [];
38
+ for (let index = start + 1; index < lines.length; index += 1) {
39
+ const line = stripGoComment(lines[index]);
40
+ if (line === ")") return { values, end: index, valid: true };
41
+ if (line !== "") values.push(line);
42
+ }
43
+ return { values, end: lines.length, valid: false };
44
+ }
45
+
46
+ function validGoDirectiveValue(name, value) {
47
+ if (!value || /\s{2,}/u.test(value)) return false;
48
+ if (name === "ignore") {
49
+ const portable = value.replaceAll("\\", "/");
50
+ const segments = portable.split("/");
51
+ return !value.includes("\\")
52
+ && !portable.startsWith("/")
53
+ && !/^[A-Za-z]:\//u.test(portable)
54
+ && !segments.includes("..")
55
+ && !value.includes("=>");
56
+ }
57
+ if (["require", "exclude"].includes(name)) return /^\S+\s+v\S+$/u.test(value);
58
+ if (name === "replace") return /^\S+(?:\s+v\S+)?\s+=>\s+\S+(?:\s+v\S+)?$/u.test(value);
59
+ if (name === "retract") return /^\S+(?:\s*,\s*\S+)?$/u.test(value);
60
+ if (name === "godebug") return /^\S+=\S+$/u.test(value);
61
+ if (name === "tool") return /^\S+(?:\s+v\S+)?$/u.test(value);
62
+ return true;
63
+ }
64
+
65
+ function parseGoModMetadataDirective(state, directive) {
66
+ if (directive.name === "module") {
67
+ if (state.module !== null || directive.value === "" || /\s/u.test(directive.value)) return false;
68
+ state.module = directive.value;
69
+ return true;
70
+ }
71
+ if (directive.name === "go") {
72
+ if (state.goVersion !== null || !/^\d+(?:\.\d+){1,2}$/u.test(directive.value)) return false;
73
+ state.goVersion = directive.value;
74
+ return true;
75
+ }
76
+ if (directive.name === "toolchain") {
77
+ if (state.toolchain !== null || !/^\S+$/u.test(directive.value)) return false;
78
+ state.toolchain = directive.value;
79
+ return true;
80
+ }
81
+ return null;
82
+ }
83
+
84
+ function parseGoModBlockDirective(lines, index, directive) {
85
+ const block = collectGoBlock(lines, index);
86
+ const valid = block.valid && block.values.every((value) => validGoDirectiveValue(directive.name, value));
87
+ return {
88
+ valid,
89
+ end: block.end,
90
+ ignorePaths: valid && directive.name === "ignore" ? block.values : [],
91
+ };
92
+ }
93
+
94
+ function parseGoModLine(lines, index, state) {
95
+ const line = stripGoComment(lines[index]);
96
+ if (line === "" || line.startsWith("//")) return { valid: true, end: index, ignorePaths: [] };
97
+ const directive = parseGoDirective(line);
98
+ if (!directive || !GO_DIRECTIVE.test(line)) return { valid: false, end: index, ignorePaths: [] };
99
+ const metadataValid = parseGoModMetadataDirective(state, directive);
100
+ if (metadataValid !== null) return { valid: metadataValid, end: index, ignorePaths: [] };
101
+ if (line.endsWith("(")) return parseGoModBlockDirective(lines, index, directive);
102
+ const valid = validGoDirectiveValue(directive.name, directive.value);
103
+ return {
104
+ valid,
105
+ end: index,
106
+ ignorePaths: valid && directive.name === "ignore" ? [directive.value] : [],
107
+ };
108
+ }
109
+
110
+ export function parseGoMod(text) {
111
+ if (typeof text !== "string" || text.length > 1024 * 1024 || /\r(?!\n)/u.test(text)) return invalidGoResult();
112
+ const lines = text.replaceAll("\r\n", "\n").split("\n");
113
+ const state = { module: null, goVersion: null, toolchain: null, valid: true };
114
+ const ignorePaths = [];
115
+ for (let index = 0; index < lines.length; index += 1) {
116
+ const parsed = parseGoModLine(lines, index, state);
117
+ state.valid &&= parsed.valid;
118
+ ignorePaths.push(...parsed.ignorePaths);
119
+ index = parsed.end;
120
+ }
121
+ return {
122
+ valid: state.valid && state.module !== null,
123
+ module: state.module,
124
+ goVersion: state.goVersion,
125
+ toolchain: state.toolchain,
126
+ uses: [],
127
+ localReplacements: [],
128
+ ignorePaths: [...new Set(ignorePaths)],
129
+ };
130
+ }
131
+
132
+ export function parseGoWork(text) {
133
+ if (typeof text !== "string" || text.length > 1024 * 1024 || /\r(?!\n)/u.test(text)) {
134
+ return { valid: false, goVersion: null, uses: [] };
135
+ }
136
+ const lines = text.replaceAll("\r\n", "\n").split("\n");
137
+ let goVersion = null;
138
+ const uses = [];
139
+ let valid = true;
140
+ for (let index = 0; index < lines.length; index += 1) {
141
+ const line = stripGoComment(lines[index]);
142
+ if (line === "") continue;
143
+ const directive = parseGoDirective(line);
144
+ if (!directive) {
145
+ valid = false;
146
+ continue;
147
+ }
148
+ if (directive.name === "go") {
149
+ if (goVersion !== null || !/^\d+(?:\.\d+){1,2}$/u.test(directive.value)) valid = false;
150
+ else goVersion = directive.value;
151
+ continue;
152
+ }
153
+ if (directive.name === "toolchain") {
154
+ if (!/^\S+$/u.test(directive.value)) valid = false;
155
+ continue;
156
+ }
157
+ if (!GO_WORK_DIRECTIVES.has(directive.name)) {
158
+ valid = false;
159
+ continue;
160
+ }
161
+ if (directive.name !== "use") {
162
+ if (line.endsWith("(")) {
163
+ const block = collectGoBlock(lines, index);
164
+ if (!block.valid || !block.values.every((value) => validGoDirectiveValue(directive.name, value))) valid = false;
165
+ index = block.end;
166
+ }
167
+ continue;
168
+ }
169
+ const values = line.endsWith("(") ? collectGoBlock(lines, index) : { values: [directive.value], end: index, valid: true };
170
+ if (!values.valid || values.values.some((value) => !validGoDirectiveValue("use", value)
171
+ || value.startsWith("/") || /^[A-Za-z]:/u.test(value))) {
172
+ valid = false;
173
+ } else {
174
+ uses.push(...values.values);
175
+ }
176
+ index = values.end;
177
+ }
178
+ return { valid: valid && goVersion !== null && uses.length > 0, goVersion, uses: [...new Set(uses)] };
179
+ }
180
+
181
+ export function inspectGoProject({ projectRoot, manifestRelative, manifestText, manifestName, targetRoot, projectFiles }) {
182
+ const parsed = manifestName === "go.work" ? parseGoWork(manifestText) : parseGoMod(manifestText);
183
+ const localUses = manifestName === "go.work"
184
+ ? parsed.uses.filter((use) => projectFiles.some((file) => file.kind === "go"
185
+ && parseGoMod(file.text).valid
186
+ && portablePath(targetRoot, path.dirname(file.path)) === path.posix.normalize(path.posix.join(projectRoot, use))))
187
+ : [];
188
+ const primary = parsed.valid && (manifestName === "go.mod" || localUses.length > 0);
189
+ const primarySignals = primary
190
+ ? [manifestName === "go.mod" ? `${manifestRelative}:module` : `${manifestRelative}:workspace`]
191
+ : [];
192
+ const supportingSignals = [];
193
+ if (parsed.goVersion) supportingSignals.push(`${manifestRelative}:go=${parsed.goVersion}`);
194
+ if (parsed.toolchain) supportingSignals.push(`${manifestRelative}:toolchain=${parsed.toolchain}`);
195
+ for (const use of localUses) supportingSignals.push(`${manifestRelative}:use=${use}`);
196
+ return createLanguageProject({
197
+ kind: "go",
198
+ root: projectRoot,
199
+ manifest: manifestRelative,
200
+ frameworks: primary ? ["go"] : [],
201
+ primary,
202
+ primarySignals,
203
+ supportingSignals,
204
+ internal: { ...parsed, valid: parsed.valid, manifestName, localUses },
205
+ });
206
+ }