@cassiomc1/forgeloop 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. package/.cursor/rules/project-loop.mdc +18 -0
  2. package/.forgeloop/.gitignore +2 -0
  3. package/.github/copilot-instructions.md +16 -0
  4. package/AGENTS.md +16 -0
  5. package/AGENT_COMPATIBILITY.md +147 -0
  6. package/CLAUDE.md +14 -0
  7. package/CONTRACT_COVERAGE.md +27 -0
  8. package/DELEGATION_PROTOCOL.md +91 -0
  9. package/ENG/accessibility-eng.md +155 -0
  10. package/ENG/clean-code-eng.md +223 -0
  11. package/ENG/design-code-eng.md +511 -0
  12. package/ENG/games-code-design-web-eng.md +751 -0
  13. package/ENG/perf-code-eng.md +441 -0
  14. package/ENG/premium-sites-studio-eng.md +320 -0
  15. package/ENG/sec-code-eng.md +706 -0
  16. package/ENG/test-code-eng.md +257 -0
  17. package/EXECUTION_STATE.md +107 -0
  18. package/GUIDE_ROUTER.md +274 -0
  19. package/LICENSE +21 -0
  20. package/LICENSE-DOCS.md +13 -0
  21. package/LOOP_ENGINEERING.md +551 -0
  22. package/LOOP_SYSTEM_DESIGN.md +394 -0
  23. package/ORCHESTRATOR_INTEGRATION.md +106 -0
  24. package/PROJECT_PROFILE.md +124 -0
  25. package/QUALITY_SCORECARD.md +54 -0
  26. package/README.md +492 -0
  27. package/TERMINOLOGY.md +21 -0
  28. package/THIRD_PARTY_NOTICES.md +129 -0
  29. package/THREAT_MODEL.md +35 -0
  30. package/package.json +51 -0
  31. package/schemas/delegated-result.schema.json +33 -0
  32. package/schemas/evidence.schema.json +15 -0
  33. package/schemas/execution-receipt.schema.json +46 -0
  34. package/schemas/routing-input.schema.json +17 -0
  35. package/schemas/routing-result.schema.json +17 -0
  36. package/schemas/task-brief.schema.json +24 -0
  37. package/schemas/work-state.schema.json +46 -0
  38. package/src/cli.js +341 -0
  39. package/src/commands/clear-state.js +11 -0
  40. package/src/commands/doctor.js +165 -0
  41. package/src/commands/init.js +42 -0
  42. package/src/commands/inspect.js +17 -0
  43. package/src/commands/route.js +32 -0
  44. package/src/commands/status.js +29 -0
  45. package/src/commands/update.js +109 -0
  46. package/src/commands/validate-protocol.js +133 -0
  47. package/src/commands/validate-receipt.js +19 -0
  48. package/src/commands/validate-state.js +30 -0
  49. package/src/core/agent-support.js +89 -0
  50. package/src/core/conformance.js +133 -0
  51. package/src/core/delegation.js +283 -0
  52. package/src/core/evidence.js +56 -0
  53. package/src/core/filesystem.js +122 -0
  54. package/src/core/inspect.js +115 -0
  55. package/src/core/json-safety.js +54 -0
  56. package/src/core/manifest.js +75 -0
  57. package/src/core/protocol.js +81 -0
  58. package/src/core/receipt.js +129 -0
  59. package/src/core/repository.js +19 -0
  60. package/src/core/router.js +296 -0
  61. package/src/core/schema-validation.js +179 -0
  62. package/src/core/templates.js +56 -0
  63. package/src/core/work-state.js +471 -0
@@ -0,0 +1,115 @@
1
+ import { fileExists, ensureWithin, readBytes } from "./filesystem.js";
2
+ import { AGENT_SUPPORT } from "./agent-support.js";
3
+ import { readManifest } from "./manifest.js";
4
+ import { PROTOCOL_VERSION } from "./protocol.js";
5
+ import { inspectSchemaHealth } from "./schema-validation.js";
6
+ import { readAndClassifyWorkState, WORK_STATE_PATH } from "./work-state.js";
7
+ import { createEvidence } from "./evidence.js";
8
+ import { runDoctor } from "../commands/doctor.js";
9
+
10
+ const PROFILE_PATH = "PROJECT_PROFILE.md";
11
+ function profileMetadata(bytes) {
12
+ const text = bytes.toString("utf8");
13
+ return {
14
+ mode: text.match(/^profile-mode:\s*([^\s]+)\s*$/m)?.[1] ?? null,
15
+ status: text.match(/^profile-status:\s*([^\s]+)\s*$/m)?.[1] ?? null,
16
+ };
17
+ }
18
+
19
+ export async function inspectTarget({ target, packageRoot, contractFile = null }) {
20
+ let manifest = null;
21
+ let manifestError = null;
22
+ try {
23
+ manifest = await readManifest(target);
24
+ } catch (error) {
25
+ manifestError = error.message;
26
+ }
27
+
28
+ const profilePath = ensureWithin(target, PROFILE_PATH);
29
+ const profile = (await fileExists(profilePath))
30
+ ? profileMetadata(await readBytes(profilePath))
31
+ : { mode: null, status: null };
32
+ const statePath = ensureWithin(target, WORK_STATE_PATH);
33
+ const statePresent = await fileExists(statePath);
34
+ const state = await readAndClassifyWorkState({ target, packageRoot, contractFile });
35
+ const schemaHealth = await inspectSchemaHealth(target);
36
+ const doctor = await runDoctor({ target, packageRoot });
37
+ const agents = await Promise.all(AGENT_SUPPORT.map(async (record) => ({
38
+ id: record.id,
39
+ name: record.name,
40
+ support: record.support,
41
+ instructionFiles: record.instructionFiles,
42
+ available: (await Promise.all(
43
+ record.instructionFiles.map(async (relativePath) => fileExists(ensureWithin(target, relativePath))),
44
+ )).some(Boolean),
45
+ })));
46
+
47
+ const findings = [...doctor.findings];
48
+ for (const schema of schemaHealth.schemas) {
49
+ if (schema.status !== "valid") {
50
+ findings.push({
51
+ code: `schema-${schema.status}`,
52
+ severity: "error",
53
+ path: `schemas/${schema.name}.schema.json`,
54
+ message: schema.error ?? `Schema is ${schema.status}.`,
55
+ remediation: "Restore the shipped schema and rerun inspect.",
56
+ evidence: createEvidence({
57
+ kind: schema.status === "missing" ? "NOT_VERIFIED" : "OBSERVED",
58
+ source: `schemas/${schema.name}.schema.json`,
59
+ result: schema.status,
60
+ }),
61
+ });
62
+ }
63
+ }
64
+ if (state.status === "INVALID") {
65
+ findings.push({
66
+ code: "state-invalid",
67
+ severity: "error",
68
+ path: WORK_STATE_PATH,
69
+ message: state.error ?? "Work state is invalid.",
70
+ remediation: "Repair or clear the checkpoint after reviewing the parse error.",
71
+ evidence: createEvidence({ kind: "BLOCKED", source: WORK_STATE_PATH, result: "invalid" }),
72
+ });
73
+ }
74
+
75
+ const protocolEvidence = schemaHealth.evidence ?? [createEvidence({
76
+ kind: schemaHealth.status === "valid" ? "OBSERVED" : "NOT_VERIFIED",
77
+ source: "ForgeLoop schema health",
78
+ result: schemaHealth.status,
79
+ })];
80
+ const evidence = [
81
+ ...(doctor.evidence ?? []),
82
+ ...(state.evidence ?? []),
83
+ ...protocolEvidence,
84
+ ];
85
+ return {
86
+ target: { path: target },
87
+ manifest: {
88
+ present: manifest !== null,
89
+ status: manifestError ? "invalid" : manifest ? "ready" : "missing",
90
+ packageVersion: manifest?.packageVersion ?? null,
91
+ error: manifestError,
92
+ },
93
+ profile,
94
+ adapters: {
95
+ detected: agents.filter((agent) => agent.available).map((agent) => agent.id),
96
+ agents,
97
+ },
98
+ protocol: {
99
+ version: PROTOCOL_VERSION,
100
+ schemaStatus: schemaHealth.status,
101
+ schemas: schemaHealth.schemas,
102
+ evidence: protocolEvidence,
103
+ },
104
+ state: { ...state, path: WORK_STATE_PATH, present: statePresent },
105
+ compatibility: {
106
+ agents: AGENT_SUPPORT.map((record) => record.id),
107
+ },
108
+ findings,
109
+ evidence,
110
+ ok: doctor.ok
111
+ && !manifestError
112
+ && schemaHealth.status === "valid"
113
+ && !["INVALID", "REVALIDATION_REQUIRED"].includes(state.status),
114
+ };
115
+ }
@@ -0,0 +1,54 @@
1
+ export const JSON_LIMITS = Object.freeze({
2
+ maxBytes: 2 * 1024 * 1024,
3
+ maxDepth: 32,
4
+ maxArrayLength: 10_000,
5
+ maxObjectKeys: 10_000,
6
+ maxStringLength: 100_000,
7
+ });
8
+
9
+ export class JsonLimitError extends Error {
10
+ constructor(message) {
11
+ super(message);
12
+ this.name = "JsonLimitError";
13
+ this.code = "JSON_LIMIT_EXCEEDED";
14
+ }
15
+ }
16
+
17
+ export function assertJsonBytes(bytes, label = "JSON artifact", limits = JSON_LIMITS) {
18
+ const size = typeof bytes === "string" ? Buffer.byteLength(bytes, "utf8") : bytes.byteLength;
19
+ if (size > limits.maxBytes) {
20
+ throw new JsonLimitError(`${label} exceeds the ${limits.maxBytes}-byte limit`);
21
+ }
22
+ return bytes;
23
+ }
24
+
25
+ export function assertJsonLimits(value, label = "JSON artifact", limits = JSON_LIMITS) {
26
+ const visited = new WeakSet();
27
+
28
+ function visit(current, depth, location) {
29
+ if (typeof current === "string") {
30
+ if (current.length > limits.maxStringLength) {
31
+ throw new JsonLimitError(`${location} exceeds the string length limit`);
32
+ }
33
+ return;
34
+ }
35
+ if (!current || typeof current !== "object") return;
36
+ if (visited.has(current)) throw new JsonLimitError(`${location} contains a circular reference`);
37
+ visited.add(current);
38
+ if (depth > limits.maxDepth) throw new JsonLimitError(`${location} exceeds the JSON depth limit`);
39
+ if (Array.isArray(current)) {
40
+ if (current.length > limits.maxArrayLength) throw new JsonLimitError(`${location} exceeds the array length limit`);
41
+ current.forEach((item, index) => visit(item, depth + 1, `${location}[${index}]`));
42
+ } else {
43
+ const keys = Object.keys(current);
44
+ if (keys.length > limits.maxObjectKeys) throw new JsonLimitError(`${location} exceeds the object key limit`);
45
+ for (const key of keys) {
46
+ if (key.length > limits.maxStringLength) throw new JsonLimitError(`${location} contains an oversized key`);
47
+ visit(current[key], depth + 1, `${location}.${key}`);
48
+ }
49
+ }
50
+ }
51
+
52
+ visit(value, 0, label);
53
+ return value;
54
+ }
@@ -0,0 +1,75 @@
1
+ import { createHash } from "node:crypto";
2
+ import { readFile } from "node:fs/promises";
3
+
4
+ import { assertSafePath, ensureWithin, fileExists, writeFileAtomic } from "./filesystem.js";
5
+ import { assertJsonBytes, assertJsonLimits } from "./json-safety.js";
6
+
7
+ export const MANIFEST_SCHEMA_VERSION = 1;
8
+ export const MANIFEST_PATH = ".forgeloop/manifest.json";
9
+ export const PACKAGE_NAME = "@cassiomc1/forgeloop";
10
+
11
+ export function sha256(bytes) {
12
+ return createHash("sha256").update(bytes).digest("hex");
13
+ }
14
+
15
+ export function createManifest(packageVersion) {
16
+ return {
17
+ schemaVersion: MANIFEST_SCHEMA_VERSION,
18
+ packageName: PACKAGE_NAME,
19
+ packageVersion,
20
+ files: {},
21
+ };
22
+ }
23
+
24
+ function validateManifest(manifest) {
25
+ if (!manifest || typeof manifest !== "object") {
26
+ throw new Error("Manifest must contain a JSON object");
27
+ }
28
+ if (manifest.schemaVersion !== MANIFEST_SCHEMA_VERSION) {
29
+ throw new Error(`Unsupported manifest schema: ${manifest.schemaVersion}`);
30
+ }
31
+ if (typeof manifest.packageVersion !== "string" || !manifest.packageVersion) {
32
+ throw new Error("Manifest packageVersion is required");
33
+ }
34
+ if (!manifest.files || typeof manifest.files !== "object" || Array.isArray(manifest.files)) {
35
+ throw new Error("Manifest files must be an object");
36
+ }
37
+
38
+ for (const [relativePath, record] of Object.entries(manifest.files)) {
39
+ ensureWithin("/manifest-root", relativePath);
40
+ if (!record || typeof record.sha256 !== "string" || !/^[a-f0-9]{64}$/.test(record.sha256)) {
41
+ throw new Error(`Invalid manifest hash for ${relativePath}`);
42
+ }
43
+ if (typeof record.preserve !== "boolean") {
44
+ throw new Error(`Invalid manifest preserve flag for ${relativePath}`);
45
+ }
46
+ }
47
+ return manifest;
48
+ }
49
+
50
+ export async function readManifest(target) {
51
+ await assertSafePath(target, MANIFEST_PATH);
52
+ const manifestPath = ensureWithin(target, MANIFEST_PATH);
53
+ if (!(await fileExists(manifestPath))) return null;
54
+ let raw;
55
+ try {
56
+ const bytes = await readFile(manifestPath);
57
+ assertJsonBytes(bytes, MANIFEST_PATH);
58
+ raw = JSON.parse(bytes.toString("utf8"));
59
+ assertJsonLimits(raw, MANIFEST_PATH);
60
+ } catch (error) {
61
+ throw new Error(`Unable to parse ${MANIFEST_PATH}: ${error.message}`);
62
+ }
63
+ return validateManifest(raw);
64
+ }
65
+
66
+ export async function writeManifest(target, manifest, { dryRun = false } = {}) {
67
+ validateManifest(manifest);
68
+ await assertSafePath(target, MANIFEST_PATH);
69
+ const manifestPath = ensureWithin(target, MANIFEST_PATH);
70
+ await writeFileAtomic(
71
+ manifestPath,
72
+ `${JSON.stringify(manifest, null, 2)}\n`,
73
+ { dryRun },
74
+ );
75
+ }
@@ -0,0 +1,81 @@
1
+ export const PROTOCOL_VERSION = 1;
2
+
3
+ export const FAILURE_CLASSES = Object.freeze([
4
+ "CONTRACT_FAILURE",
5
+ "DISCOVERY_FAILURE",
6
+ "ROUTING_FAILURE",
7
+ "IMPLEMENTATION_FAILURE",
8
+ "VERIFICATION_FAILURE",
9
+ "REGRESSION_FAILURE",
10
+ "REVIEW_FAILURE",
11
+ "CAPABILITY_FAILURE",
12
+ "AUTHORITY_FAILURE",
13
+ "ENVIRONMENT_FAILURE",
14
+ "EXTERNAL_SERVICE_FAILURE",
15
+ "STALE_STATE_FAILURE",
16
+ ]);
17
+
18
+ export const WORK_PHASES = Object.freeze([
19
+ "RECEIVED",
20
+ "DISCOVERING",
21
+ "CONTRACT_READY",
22
+ "ROUTED",
23
+ "DESIGNING",
24
+ "PLANNED",
25
+ "EXECUTING",
26
+ "VERIFYING",
27
+ "DIAGNOSING",
28
+ "CORRECTING",
29
+ "REVIEWING",
30
+ "COMPLETE",
31
+ "BLOCKED",
32
+ ]);
33
+
34
+ export const GUIDE_IDS = Object.freeze([
35
+ "premium",
36
+ "clean",
37
+ "test",
38
+ "security",
39
+ "design",
40
+ "performance",
41
+ "accessibility",
42
+ "games",
43
+ ]);
44
+
45
+ export const GUIDE_ORDER = GUIDE_IDS;
46
+
47
+ export const WORK_TRANSITIONS = Object.freeze({
48
+ RECEIVED: ["DISCOVERING"],
49
+ DISCOVERING: ["CONTRACT_READY"],
50
+ CONTRACT_READY: ["ROUTED"],
51
+ ROUTED: ["DESIGNING", "PLANNED"],
52
+ DESIGNING: ["PLANNED"],
53
+ PLANNED: ["EXECUTING"],
54
+ EXECUTING: ["VERIFYING"],
55
+ VERIFYING: ["DIAGNOSING", "REVIEWING"],
56
+ DIAGNOSING: ["CORRECTING"],
57
+ CORRECTING: ["VERIFYING"],
58
+ REVIEWING: ["COMPLETE", "CORRECTING"],
59
+ COMPLETE: [],
60
+ BLOCKED: [],
61
+ });
62
+
63
+ export function isValidTransition(from, to) {
64
+ if (!WORK_PHASES.includes(from) || !WORK_PHASES.includes(to)) return false;
65
+ if (to === "BLOCKED" && from !== "COMPLETE" && from !== "BLOCKED") return true;
66
+ return WORK_TRANSITIONS[from].includes(to);
67
+ }
68
+
69
+ export function assertFailureClass(value) {
70
+ if (!FAILURE_CLASSES.includes(value)) {
71
+ throw new Error(`Unknown failure class: ${value}`);
72
+ }
73
+ return value;
74
+ }
75
+
76
+ export function assertWorkPhase(value) {
77
+ if (!WORK_PHASES.includes(value)) {
78
+ throw new Error(`Unknown work phase: ${value}`);
79
+ }
80
+ return value;
81
+ }
@@ -0,0 +1,129 @@
1
+ import { GUIDE_IDS, PROTOCOL_VERSION } from "./protocol.js";
2
+ import { assertSchema, readSchema } from "./schema-validation.js";
3
+ import { assertEvidenceList, evidenceMatches } from "./evidence.js";
4
+ import { assertJsonLimits } from "./json-safety.js";
5
+
6
+ const RECEIPT_SCHEMA_VERSION = 1;
7
+ const SECRET_WORDS = new Set(["token", "password", "secret", "credential"]);
8
+ const SECRET_VALUE_PATTERNS = [
9
+ /-----BEGIN [A-Z ]*PRIVATE KEY-----/i,
10
+ /(?:^|\s)(?:sk|ghp|glpat|xox[baprs])-[-_a-z0-9]{8,}/i,
11
+ /(?:AKIA|ASIA)[A-Z0-9]{12,}/,
12
+ ];
13
+
14
+ function findSecretLikeValues(value, location, violations) {
15
+ if (Array.isArray(value)) {
16
+ value.forEach((item, index) => findSecretLikeValues(item, `${location}[${index}]`, violations));
17
+ return;
18
+ }
19
+ if (!value || typeof value !== "object") {
20
+ if (typeof value === "string" && SECRET_VALUE_PATTERNS.some((pattern) => pattern.test(value))) {
21
+ violations.push(`${location}: secret-like value is not allowed`);
22
+ }
23
+ return;
24
+ }
25
+
26
+ for (const [key, child] of Object.entries(value)) {
27
+ const childLocation = `${location}.${key}`;
28
+ const pathLikeDocumentation = /\.(?:md|txt|json)$/i.test(key);
29
+ const words = key.split(/(?=[A-Z])|[_\-\s]+/).filter(Boolean).map((word) => word.toLowerCase());
30
+ const compact = key.replaceAll(/[^a-z0-9]/gi, "").toLowerCase();
31
+ const containsSensitiveWord = !pathLikeDocumentation && (
32
+ words.some((word) => SECRET_WORDS.has(word))
33
+ || ["apikey", "privatekey", "accesskey", "secretkey"].some((term) => compact.includes(term))
34
+ || words.some((word, index) => ["api", "private", "access", "secret"].includes(word)
35
+ && ["key", "token"].includes(words[index + 1]))
36
+ );
37
+ if (containsSensitiveWord) {
38
+ violations.push(`${childLocation}: secret-like field is not allowed`);
39
+ }
40
+ findSecretLikeValues(child, childLocation, violations);
41
+ }
42
+ }
43
+
44
+ export function assertSecretFree(value) {
45
+ assertJsonLimits(value, "secret-free artifact");
46
+ const violations = [];
47
+ findSecretLikeValues(value, "$", violations);
48
+ if (violations.length > 0) throw new Error(violations.join("; "));
49
+ }
50
+
51
+ function assertKnownGuides(guides) {
52
+ const unknown = guides.filter((guide) => !GUIDE_IDS.includes(guide));
53
+ if (unknown.length > 0) throw new Error(`Receipt contains unknown guide: ${unknown[0]}`);
54
+ if (new Set(guides).size !== guides.length) throw new Error("Receipt selectedGuides must not contain duplicates");
55
+ }
56
+
57
+ export function assertReceiptSemantics(receipt) {
58
+ const evidence = receipt.evidence ?? [];
59
+ assertEvidenceList(evidence, "receipt.evidence");
60
+
61
+ for (const [index, check] of receipt.checks.entries()) {
62
+ if (check?.status === "passed") {
63
+ const hasCommandOrResult = [check.command, check.result, check.name]
64
+ .some((value) => typeof value === "string" && value.trim().length > 0);
65
+ if (!hasCommandOrResult) {
66
+ throw new Error(`receipt.checks[${index}] passed check requires a command or result`);
67
+ }
68
+ }
69
+ }
70
+
71
+ if (receipt.status === "complete") {
72
+ const verificationEvidence = evidence.filter((item) => ["OBSERVED", "INFERRED"].includes(item.kind));
73
+ if (verificationEvidence.length === 0) {
74
+ throw new Error("COMPLETE receipt requires verification evidence");
75
+ }
76
+ }
77
+
78
+ const publicationEvidence = [
79
+ [receipt.publication.committed, ["commit", "committed"], "committed", "commit"],
80
+ [receipt.publication.pushed, ["git push", "pushed"], "pushed", "push"],
81
+ [receipt.publication.deployed, ["deploy", "deployed", "deployment"], "deployed", "deployment"],
82
+ ];
83
+ for (const [claimed, terms, field, label] of publicationEvidence) {
84
+ if (claimed && !evidenceMatches(evidence, terms)) {
85
+ throw new Error(`publication.${field} requires ${label} evidence`);
86
+ }
87
+ }
88
+
89
+ if (receipt.review.independent === true) {
90
+ const implementer = receipt.review.implementerId;
91
+ const reviewer = receipt.review.reviewerId;
92
+ if (typeof implementer !== "string" || typeof reviewer !== "string" || implementer === reviewer) {
93
+ throw new Error("independent review requires distinct implementer and reviewer identities");
94
+ }
95
+ }
96
+ return receipt;
97
+ }
98
+
99
+ export async function validateReceipt(receipt, packageRoot) {
100
+ assertSecretFree(receipt);
101
+ const schema = await readSchema("execution-receipt", packageRoot);
102
+ assertSchema(receipt, schema, "execution receipt");
103
+ assertKnownGuides(receipt.selectedGuides);
104
+ return assertReceiptSemantics(receipt);
105
+ }
106
+
107
+ export async function createReceipt(input, packageRoot) {
108
+ assertSecretFree(input);
109
+ const receipt = {
110
+ schemaVersion: RECEIPT_SCHEMA_VERSION,
111
+ protocolVersion: PROTOCOL_VERSION,
112
+ taskId: input.taskId,
113
+ contractFingerprint: input.contractFingerprint,
114
+ status: input.status ?? "in-progress",
115
+ selectedGuides: [...(input.selectedGuides ?? [])],
116
+ changedPaths: [...(input.changedPaths ?? [])],
117
+ checks: [...(input.checks ?? [])],
118
+ evidence: [...(input.evidence ?? [])],
119
+ review: input.review ?? { status: "not-run", independent: false },
120
+ limitations: [...(input.limitations ?? [])],
121
+ publication: input.publication ?? {
122
+ committed: false,
123
+ pushed: false,
124
+ pullRequest: null,
125
+ deployed: false,
126
+ },
127
+ };
128
+ return validateReceipt(receipt, packageRoot);
129
+ }
@@ -0,0 +1,19 @@
1
+ import { execFile } from "node:child_process";
2
+ import { promisify } from "node:util";
3
+
4
+ const execFileAsync = promisify(execFile);
5
+
6
+ export async function currentRepositoryFingerprint(target) {
7
+ try {
8
+ const [{ stdout: branchOutput }, { stdout: headOutput }] = await Promise.all([
9
+ execFileAsync("git", ["-C", target, "branch", "--show-current"], { windowsHide: true }),
10
+ execFileAsync("git", ["-C", target, "rev-parse", "HEAD"], { windowsHide: true }),
11
+ ]);
12
+ return {
13
+ branch: branchOutput.trim() || null,
14
+ head: headOutput.trim() || null,
15
+ };
16
+ } catch {
17
+ return { branch: null, head: null };
18
+ }
19
+ }