@harness-engineering/core 0.12.0 → 0.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/dist/architecture/matchers.d.mts +1 -1
- package/dist/architecture/matchers.d.ts +1 -1
- package/dist/index.d.mts +484 -358
- package/dist/index.d.ts +484 -358
- package/dist/index.js +638 -271
- package/dist/index.mjs +609 -254
- package/dist/{matchers-D20x48U9.d.mts → matchers-Dj1t5vpg.d.mts} +46 -46
- package/dist/{matchers-D20x48U9.d.ts → matchers-Dj1t5vpg.d.ts} +46 -46
- package/package.json +3 -3
package/dist/index.mjs
CHANGED
|
@@ -83,15 +83,15 @@ function validateConfig(data, schema) {
|
|
|
83
83
|
let message = "Configuration validation failed";
|
|
84
84
|
const suggestions = [];
|
|
85
85
|
if (firstError) {
|
|
86
|
-
const
|
|
87
|
-
const pathDisplay =
|
|
86
|
+
const path20 = firstError.path.join(".");
|
|
87
|
+
const pathDisplay = path20 ? ` at "${path20}"` : "";
|
|
88
88
|
if (firstError.code === "invalid_type") {
|
|
89
89
|
const received = firstError.received;
|
|
90
90
|
const expected = firstError.expected;
|
|
91
91
|
if (received === "undefined") {
|
|
92
92
|
code = "MISSING_FIELD";
|
|
93
93
|
message = `Missing required field${pathDisplay}: ${firstError.message}`;
|
|
94
|
-
suggestions.push(`Field "${
|
|
94
|
+
suggestions.push(`Field "${path20}" is required and must be of type "${expected}"`);
|
|
95
95
|
} else {
|
|
96
96
|
code = "INVALID_TYPE";
|
|
97
97
|
message = `Invalid type${pathDisplay}: ${firstError.message}`;
|
|
@@ -304,30 +304,27 @@ function extractSections(content) {
|
|
|
304
304
|
return result;
|
|
305
305
|
});
|
|
306
306
|
}
|
|
307
|
-
function isExternalLink(
|
|
308
|
-
return
|
|
307
|
+
function isExternalLink(path20) {
|
|
308
|
+
return path20.startsWith("http://") || path20.startsWith("https://") || path20.startsWith("#") || path20.startsWith("mailto:");
|
|
309
309
|
}
|
|
310
310
|
function resolveLinkPath(linkPath, baseDir) {
|
|
311
311
|
return linkPath.startsWith(".") ? join(baseDir, linkPath) : linkPath;
|
|
312
312
|
}
|
|
313
|
-
async function validateAgentsMap(
|
|
314
|
-
|
|
315
|
-
"[harness] validateAgentsMap() is deprecated. Use graph-based validation via Assembler.checkCoverage() from @harness-engineering/graph"
|
|
316
|
-
);
|
|
317
|
-
const contentResult = await readFileContent(path13);
|
|
313
|
+
async function validateAgentsMap(path20 = "./AGENTS.md") {
|
|
314
|
+
const contentResult = await readFileContent(path20);
|
|
318
315
|
if (!contentResult.ok) {
|
|
319
316
|
return Err(
|
|
320
317
|
createError(
|
|
321
318
|
"PARSE_ERROR",
|
|
322
319
|
`Failed to read AGENTS.md: ${contentResult.error.message}`,
|
|
323
|
-
{ path:
|
|
320
|
+
{ path: path20 },
|
|
324
321
|
["Ensure the file exists", "Check file permissions"]
|
|
325
322
|
)
|
|
326
323
|
);
|
|
327
324
|
}
|
|
328
325
|
const content = contentResult.value;
|
|
329
326
|
const sections = extractSections(content);
|
|
330
|
-
const baseDir = dirname(
|
|
327
|
+
const baseDir = dirname(path20);
|
|
331
328
|
const sectionTitles = sections.map((s) => s.title);
|
|
332
329
|
const missingSections = REQUIRED_SECTIONS.filter(
|
|
333
330
|
(required) => !sectionTitles.some((title) => title.toLowerCase().includes(required.toLowerCase()))
|
|
@@ -468,8 +465,8 @@ async function checkDocCoverage(domain, options = {}) {
|
|
|
468
465
|
|
|
469
466
|
// src/context/knowledge-map.ts
|
|
470
467
|
import { join as join2, basename as basename2, relative as relative2 } from "path";
|
|
471
|
-
function suggestFix(
|
|
472
|
-
const targetName = basename2(
|
|
468
|
+
function suggestFix(path20, existingFiles) {
|
|
469
|
+
const targetName = basename2(path20).toLowerCase();
|
|
473
470
|
const similar = existingFiles.find((file) => {
|
|
474
471
|
const fileName = basename2(file).toLowerCase();
|
|
475
472
|
return fileName.includes(targetName) || targetName.includes(fileName);
|
|
@@ -477,12 +474,9 @@ function suggestFix(path13, existingFiles) {
|
|
|
477
474
|
if (similar) {
|
|
478
475
|
return `Did you mean "${similar}"?`;
|
|
479
476
|
}
|
|
480
|
-
return `Create the file "${
|
|
477
|
+
return `Create the file "${path20}" or remove the link`;
|
|
481
478
|
}
|
|
482
479
|
async function validateKnowledgeMap(rootDir = process.cwd()) {
|
|
483
|
-
console.warn(
|
|
484
|
-
"[harness] validateKnowledgeMap() is deprecated. Use graph-based validation via Assembler.checkCoverage() from @harness-engineering/graph"
|
|
485
|
-
);
|
|
486
480
|
const agentsPath = join2(rootDir, "AGENTS.md");
|
|
487
481
|
const agentsResult = await validateAgentsMap(agentsPath);
|
|
488
482
|
if (!agentsResult.ok) {
|
|
@@ -832,8 +826,8 @@ function createBoundaryValidator(schema, name) {
|
|
|
832
826
|
return Ok(result.data);
|
|
833
827
|
}
|
|
834
828
|
const suggestions = result.error.issues.map((issue) => {
|
|
835
|
-
const
|
|
836
|
-
return
|
|
829
|
+
const path20 = issue.path.join(".");
|
|
830
|
+
return path20 ? `${path20}: ${issue.message}` : issue.message;
|
|
837
831
|
});
|
|
838
832
|
return Err(
|
|
839
833
|
createError(
|
|
@@ -1387,11 +1381,11 @@ function walk(node, visitor) {
|
|
|
1387
1381
|
var TypeScriptParser = class {
|
|
1388
1382
|
name = "typescript";
|
|
1389
1383
|
extensions = [".ts", ".tsx", ".mts", ".cts"];
|
|
1390
|
-
async parseFile(
|
|
1391
|
-
const contentResult = await readFileContent(
|
|
1384
|
+
async parseFile(path20) {
|
|
1385
|
+
const contentResult = await readFileContent(path20);
|
|
1392
1386
|
if (!contentResult.ok) {
|
|
1393
1387
|
return Err(
|
|
1394
|
-
createParseError("NOT_FOUND", `File not found: ${
|
|
1388
|
+
createParseError("NOT_FOUND", `File not found: ${path20}`, { path: path20 }, [
|
|
1395
1389
|
"Check that the file exists",
|
|
1396
1390
|
"Verify the path is correct"
|
|
1397
1391
|
])
|
|
@@ -1401,7 +1395,7 @@ var TypeScriptParser = class {
|
|
|
1401
1395
|
const ast = parse(contentResult.value, {
|
|
1402
1396
|
loc: true,
|
|
1403
1397
|
range: true,
|
|
1404
|
-
jsx:
|
|
1398
|
+
jsx: path20.endsWith(".tsx"),
|
|
1405
1399
|
errorOnUnknownASTType: false
|
|
1406
1400
|
});
|
|
1407
1401
|
return Ok({
|
|
@@ -1412,7 +1406,7 @@ var TypeScriptParser = class {
|
|
|
1412
1406
|
} catch (e) {
|
|
1413
1407
|
const error = e;
|
|
1414
1408
|
return Err(
|
|
1415
|
-
createParseError("SYNTAX_ERROR", `Failed to parse ${
|
|
1409
|
+
createParseError("SYNTAX_ERROR", `Failed to parse ${path20}: ${error.message}`, { path: path20 }, [
|
|
1416
1410
|
"Check for syntax errors in the file",
|
|
1417
1411
|
"Ensure valid TypeScript syntax"
|
|
1418
1412
|
])
|
|
@@ -1696,22 +1690,22 @@ function extractInlineRefs(content) {
|
|
|
1696
1690
|
}
|
|
1697
1691
|
return refs;
|
|
1698
1692
|
}
|
|
1699
|
-
async function parseDocumentationFile(
|
|
1700
|
-
const contentResult = await readFileContent(
|
|
1693
|
+
async function parseDocumentationFile(path20) {
|
|
1694
|
+
const contentResult = await readFileContent(path20);
|
|
1701
1695
|
if (!contentResult.ok) {
|
|
1702
1696
|
return Err(
|
|
1703
1697
|
createEntropyError(
|
|
1704
1698
|
"PARSE_ERROR",
|
|
1705
|
-
`Failed to read documentation file: ${
|
|
1706
|
-
{ file:
|
|
1699
|
+
`Failed to read documentation file: ${path20}`,
|
|
1700
|
+
{ file: path20 },
|
|
1707
1701
|
["Check that the file exists"]
|
|
1708
1702
|
)
|
|
1709
1703
|
);
|
|
1710
1704
|
}
|
|
1711
1705
|
const content = contentResult.value;
|
|
1712
|
-
const type =
|
|
1706
|
+
const type = path20.endsWith(".md") ? "markdown" : "text";
|
|
1713
1707
|
return Ok({
|
|
1714
|
-
path:
|
|
1708
|
+
path: path20,
|
|
1715
1709
|
type,
|
|
1716
1710
|
content,
|
|
1717
1711
|
codeBlocks: extractCodeBlocks(content),
|
|
@@ -2522,15 +2516,34 @@ async function detectPatternViolations(snapshot, config) {
|
|
|
2522
2516
|
}
|
|
2523
2517
|
}
|
|
2524
2518
|
}
|
|
2519
|
+
if (config?.customPatterns) {
|
|
2520
|
+
for (const file of snapshot.files) {
|
|
2521
|
+
for (const custom of config.customPatterns) {
|
|
2522
|
+
const matches = custom.check(file, snapshot);
|
|
2523
|
+
for (const match of matches) {
|
|
2524
|
+
violations.push({
|
|
2525
|
+
pattern: custom.name,
|
|
2526
|
+
file: file.path,
|
|
2527
|
+
line: match.line,
|
|
2528
|
+
message: match.message,
|
|
2529
|
+
suggestion: match.suggestion || "Review and fix this pattern violation",
|
|
2530
|
+
severity: custom.severity
|
|
2531
|
+
});
|
|
2532
|
+
}
|
|
2533
|
+
}
|
|
2534
|
+
}
|
|
2535
|
+
}
|
|
2525
2536
|
const errorCount = violations.filter((v) => v.severity === "error").length;
|
|
2526
2537
|
const warningCount = violations.filter((v) => v.severity === "warning").length;
|
|
2527
|
-
const
|
|
2528
|
-
const
|
|
2538
|
+
const customCount = config?.customPatterns?.length ?? 0;
|
|
2539
|
+
const allPatternsCount = patterns.length + customCount;
|
|
2540
|
+
const totalChecks = snapshot.files.length * allPatternsCount;
|
|
2541
|
+
const passRate = totalChecks > 0 ? Math.max(0, (totalChecks - violations.length) / totalChecks) : 1;
|
|
2529
2542
|
return Ok({
|
|
2530
2543
|
violations,
|
|
2531
2544
|
stats: {
|
|
2532
2545
|
filesChecked: snapshot.files.length,
|
|
2533
|
-
patternsApplied:
|
|
2546
|
+
patternsApplied: allPatternsCount,
|
|
2534
2547
|
violationCount: violations.length,
|
|
2535
2548
|
errorCount,
|
|
2536
2549
|
warningCount
|
|
@@ -4713,10 +4726,13 @@ var DEFAULT_STATE = {
|
|
|
4713
4726
|
progress: {}
|
|
4714
4727
|
};
|
|
4715
4728
|
|
|
4716
|
-
// src/state/state-
|
|
4717
|
-
import * as
|
|
4718
|
-
import * as
|
|
4719
|
-
|
|
4729
|
+
// src/state/state-persistence.ts
|
|
4730
|
+
import * as fs8 from "fs";
|
|
4731
|
+
import * as path5 from "path";
|
|
4732
|
+
|
|
4733
|
+
// src/state/state-shared.ts
|
|
4734
|
+
import * as fs7 from "fs";
|
|
4735
|
+
import * as path4 from "path";
|
|
4720
4736
|
|
|
4721
4737
|
// src/state/stream-resolver.ts
|
|
4722
4738
|
import * as fs5 from "fs";
|
|
@@ -4742,10 +4758,20 @@ var DEFAULT_STREAM_INDEX = {
|
|
|
4742
4758
|
streams: {}
|
|
4743
4759
|
};
|
|
4744
4760
|
|
|
4745
|
-
// src/state/
|
|
4761
|
+
// src/state/constants.ts
|
|
4746
4762
|
var HARNESS_DIR = ".harness";
|
|
4747
|
-
var
|
|
4763
|
+
var STATE_FILE = "state.json";
|
|
4764
|
+
var LEARNINGS_FILE = "learnings.md";
|
|
4765
|
+
var FAILURES_FILE = "failures.md";
|
|
4766
|
+
var HANDOFF_FILE = "handoff.json";
|
|
4767
|
+
var GATE_CONFIG_FILE = "gate.json";
|
|
4748
4768
|
var INDEX_FILE = "index.json";
|
|
4769
|
+
var SESSIONS_DIR = "sessions";
|
|
4770
|
+
var SESSION_INDEX_FILE = "index.md";
|
|
4771
|
+
var SUMMARY_FILE = "summary.md";
|
|
4772
|
+
|
|
4773
|
+
// src/state/stream-resolver.ts
|
|
4774
|
+
var STREAMS_DIR = "streams";
|
|
4749
4775
|
var STREAM_NAME_REGEX = /^[a-z0-9][a-z0-9._-]*$/;
|
|
4750
4776
|
function streamsDir(projectPath) {
|
|
4751
4777
|
return path2.join(projectPath, HARNESS_DIR, STREAMS_DIR);
|
|
@@ -4972,26 +4998,65 @@ async function migrateToStreams(projectPath) {
|
|
|
4972
4998
|
return saveStreamIndex(projectPath, index);
|
|
4973
4999
|
}
|
|
4974
5000
|
|
|
4975
|
-
// src/state/
|
|
4976
|
-
|
|
4977
|
-
|
|
4978
|
-
|
|
4979
|
-
|
|
4980
|
-
|
|
4981
|
-
|
|
4982
|
-
|
|
5001
|
+
// src/state/session-resolver.ts
|
|
5002
|
+
import * as fs6 from "fs";
|
|
5003
|
+
import * as path3 from "path";
|
|
5004
|
+
function resolveSessionDir(projectPath, sessionSlug, options) {
|
|
5005
|
+
if (!sessionSlug || sessionSlug.trim() === "") {
|
|
5006
|
+
return Err(new Error("Session slug must not be empty"));
|
|
5007
|
+
}
|
|
5008
|
+
if (sessionSlug.includes("..") || sessionSlug.includes("/") || sessionSlug.includes("\\")) {
|
|
5009
|
+
return Err(
|
|
5010
|
+
new Error(`Invalid session slug '${sessionSlug}': must not contain path traversal characters`)
|
|
5011
|
+
);
|
|
5012
|
+
}
|
|
5013
|
+
const sessionDir = path3.join(projectPath, HARNESS_DIR, SESSIONS_DIR, sessionSlug);
|
|
5014
|
+
if (options?.create) {
|
|
5015
|
+
fs6.mkdirSync(sessionDir, { recursive: true });
|
|
5016
|
+
}
|
|
5017
|
+
return Ok(sessionDir);
|
|
5018
|
+
}
|
|
5019
|
+
function updateSessionIndex(projectPath, sessionSlug, description) {
|
|
5020
|
+
const sessionsDir = path3.join(projectPath, HARNESS_DIR, SESSIONS_DIR);
|
|
5021
|
+
fs6.mkdirSync(sessionsDir, { recursive: true });
|
|
5022
|
+
const indexPath2 = path3.join(sessionsDir, SESSION_INDEX_FILE);
|
|
5023
|
+
const date = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
|
|
5024
|
+
const newLine = `- [${sessionSlug}](${sessionSlug}/summary.md) \u2014 ${description} (${date})`;
|
|
5025
|
+
if (!fs6.existsSync(indexPath2)) {
|
|
5026
|
+
fs6.writeFileSync(indexPath2, `## Active Sessions
|
|
5027
|
+
|
|
5028
|
+
${newLine}
|
|
5029
|
+
`);
|
|
5030
|
+
return;
|
|
5031
|
+
}
|
|
5032
|
+
const content = fs6.readFileSync(indexPath2, "utf-8");
|
|
5033
|
+
const lines = content.split("\n");
|
|
5034
|
+
const slugPattern = `- [${sessionSlug}]`;
|
|
5035
|
+
const existingIdx = lines.findIndex((l) => l.startsWith(slugPattern));
|
|
5036
|
+
if (existingIdx >= 0) {
|
|
5037
|
+
lines[existingIdx] = newLine;
|
|
5038
|
+
} else {
|
|
5039
|
+
const lastNonEmpty = lines.reduce((last, line, i) => line.trim() !== "" ? i : last, 0);
|
|
5040
|
+
lines.splice(lastNonEmpty + 1, 0, newLine);
|
|
5041
|
+
}
|
|
5042
|
+
fs6.writeFileSync(indexPath2, lines.join("\n"));
|
|
5043
|
+
}
|
|
5044
|
+
|
|
5045
|
+
// src/state/state-shared.ts
|
|
4983
5046
|
var MAX_CACHE_ENTRIES = 8;
|
|
4984
|
-
var learningsCacheMap = /* @__PURE__ */ new Map();
|
|
4985
|
-
var failuresCacheMap = /* @__PURE__ */ new Map();
|
|
4986
5047
|
function evictIfNeeded(map) {
|
|
4987
5048
|
if (map.size > MAX_CACHE_ENTRIES) {
|
|
4988
5049
|
const oldest = map.keys().next().value;
|
|
4989
5050
|
if (oldest !== void 0) map.delete(oldest);
|
|
4990
5051
|
}
|
|
4991
5052
|
}
|
|
4992
|
-
async function getStateDir(projectPath, stream) {
|
|
4993
|
-
|
|
4994
|
-
|
|
5053
|
+
async function getStateDir(projectPath, stream, session) {
|
|
5054
|
+
if (session) {
|
|
5055
|
+
const sessionResult = resolveSessionDir(projectPath, session, { create: true });
|
|
5056
|
+
return sessionResult;
|
|
5057
|
+
}
|
|
5058
|
+
const streamsIndexPath = path4.join(projectPath, HARNESS_DIR, "streams", INDEX_FILE);
|
|
5059
|
+
const hasStreams = fs7.existsSync(streamsIndexPath);
|
|
4995
5060
|
if (stream || hasStreams) {
|
|
4996
5061
|
const result = await resolveStreamPath(projectPath, stream ? { stream } : void 0);
|
|
4997
5062
|
if (result.ok) {
|
|
@@ -5001,18 +5066,20 @@ async function getStateDir(projectPath, stream) {
|
|
|
5001
5066
|
return result;
|
|
5002
5067
|
}
|
|
5003
5068
|
}
|
|
5004
|
-
return Ok(
|
|
5069
|
+
return Ok(path4.join(projectPath, HARNESS_DIR));
|
|
5005
5070
|
}
|
|
5006
|
-
|
|
5071
|
+
|
|
5072
|
+
// src/state/state-persistence.ts
|
|
5073
|
+
async function loadState(projectPath, stream, session) {
|
|
5007
5074
|
try {
|
|
5008
|
-
const dirResult = await getStateDir(projectPath, stream);
|
|
5075
|
+
const dirResult = await getStateDir(projectPath, stream, session);
|
|
5009
5076
|
if (!dirResult.ok) return dirResult;
|
|
5010
5077
|
const stateDir = dirResult.value;
|
|
5011
|
-
const statePath =
|
|
5012
|
-
if (!
|
|
5078
|
+
const statePath = path5.join(stateDir, STATE_FILE);
|
|
5079
|
+
if (!fs8.existsSync(statePath)) {
|
|
5013
5080
|
return Ok({ ...DEFAULT_STATE });
|
|
5014
5081
|
}
|
|
5015
|
-
const raw =
|
|
5082
|
+
const raw = fs8.readFileSync(statePath, "utf-8");
|
|
5016
5083
|
const parsed = JSON.parse(raw);
|
|
5017
5084
|
const result = HarnessStateSchema.safeParse(parsed);
|
|
5018
5085
|
if (!result.success) {
|
|
@@ -5025,14 +5092,14 @@ async function loadState(projectPath, stream) {
|
|
|
5025
5092
|
);
|
|
5026
5093
|
}
|
|
5027
5094
|
}
|
|
5028
|
-
async function saveState(projectPath, state, stream) {
|
|
5095
|
+
async function saveState(projectPath, state, stream, session) {
|
|
5029
5096
|
try {
|
|
5030
|
-
const dirResult = await getStateDir(projectPath, stream);
|
|
5097
|
+
const dirResult = await getStateDir(projectPath, stream, session);
|
|
5031
5098
|
if (!dirResult.ok) return dirResult;
|
|
5032
5099
|
const stateDir = dirResult.value;
|
|
5033
|
-
const statePath =
|
|
5034
|
-
|
|
5035
|
-
|
|
5100
|
+
const statePath = path5.join(stateDir, STATE_FILE);
|
|
5101
|
+
fs8.mkdirSync(stateDir, { recursive: true });
|
|
5102
|
+
fs8.writeFileSync(statePath, JSON.stringify(state, null, 2));
|
|
5036
5103
|
return Ok(void 0);
|
|
5037
5104
|
} catch (error) {
|
|
5038
5105
|
return Err(
|
|
@@ -5040,13 +5107,21 @@ async function saveState(projectPath, state, stream) {
|
|
|
5040
5107
|
);
|
|
5041
5108
|
}
|
|
5042
5109
|
}
|
|
5043
|
-
|
|
5110
|
+
|
|
5111
|
+
// src/state/learnings.ts
|
|
5112
|
+
import * as fs9 from "fs";
|
|
5113
|
+
import * as path6 from "path";
|
|
5114
|
+
var learningsCacheMap = /* @__PURE__ */ new Map();
|
|
5115
|
+
function clearLearningsCache() {
|
|
5116
|
+
learningsCacheMap.clear();
|
|
5117
|
+
}
|
|
5118
|
+
async function appendLearning(projectPath, learning, skillName, outcome, stream, session) {
|
|
5044
5119
|
try {
|
|
5045
|
-
const dirResult = await getStateDir(projectPath, stream);
|
|
5120
|
+
const dirResult = await getStateDir(projectPath, stream, session);
|
|
5046
5121
|
if (!dirResult.ok) return dirResult;
|
|
5047
5122
|
const stateDir = dirResult.value;
|
|
5048
|
-
const learningsPath =
|
|
5049
|
-
|
|
5123
|
+
const learningsPath = path6.join(stateDir, LEARNINGS_FILE);
|
|
5124
|
+
fs9.mkdirSync(stateDir, { recursive: true });
|
|
5050
5125
|
const timestamp = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
|
|
5051
5126
|
let entry;
|
|
5052
5127
|
if (skillName && outcome) {
|
|
@@ -5062,11 +5137,11 @@ async function appendLearning(projectPath, learning, skillName, outcome, stream)
|
|
|
5062
5137
|
- **${timestamp}:** ${learning}
|
|
5063
5138
|
`;
|
|
5064
5139
|
}
|
|
5065
|
-
if (!
|
|
5066
|
-
|
|
5140
|
+
if (!fs9.existsSync(learningsPath)) {
|
|
5141
|
+
fs9.writeFileSync(learningsPath, `# Learnings
|
|
5067
5142
|
${entry}`);
|
|
5068
5143
|
} else {
|
|
5069
|
-
|
|
5144
|
+
fs9.appendFileSync(learningsPath, entry);
|
|
5070
5145
|
}
|
|
5071
5146
|
learningsCacheMap.delete(learningsPath);
|
|
5072
5147
|
return Ok(void 0);
|
|
@@ -5078,23 +5153,92 @@ ${entry}`);
|
|
|
5078
5153
|
);
|
|
5079
5154
|
}
|
|
5080
5155
|
}
|
|
5081
|
-
|
|
5156
|
+
function estimateTokens(text) {
|
|
5157
|
+
return Math.ceil(text.length / 4);
|
|
5158
|
+
}
|
|
5159
|
+
function scoreRelevance(entry, intent) {
|
|
5160
|
+
if (!intent || intent.trim() === "") return 0;
|
|
5161
|
+
const intentWords = intent.toLowerCase().split(/\s+/).filter((w) => w.length > 2);
|
|
5162
|
+
if (intentWords.length === 0) return 0;
|
|
5163
|
+
const entryLower = entry.toLowerCase();
|
|
5164
|
+
const matches = intentWords.filter((word) => entryLower.includes(word));
|
|
5165
|
+
return matches.length / intentWords.length;
|
|
5166
|
+
}
|
|
5167
|
+
function parseDateFromEntry(entry) {
|
|
5168
|
+
const match = entry.match(/(\d{4}-\d{2}-\d{2})/);
|
|
5169
|
+
return match ? match[1] ?? null : null;
|
|
5170
|
+
}
|
|
5171
|
+
function analyzeLearningPatterns(entries) {
|
|
5172
|
+
const tagGroups = /* @__PURE__ */ new Map();
|
|
5173
|
+
for (const entry of entries) {
|
|
5174
|
+
const tagMatches = entry.matchAll(/\[(skill:[^\]]+)\]|\[(outcome:[^\]]+)\]/g);
|
|
5175
|
+
for (const match of tagMatches) {
|
|
5176
|
+
const tag = match[1] ?? match[2];
|
|
5177
|
+
if (tag) {
|
|
5178
|
+
const group = tagGroups.get(tag) ?? [];
|
|
5179
|
+
group.push(entry);
|
|
5180
|
+
tagGroups.set(tag, group);
|
|
5181
|
+
}
|
|
5182
|
+
}
|
|
5183
|
+
}
|
|
5184
|
+
const patterns = [];
|
|
5185
|
+
for (const [tag, groupEntries] of tagGroups) {
|
|
5186
|
+
if (groupEntries.length >= 3) {
|
|
5187
|
+
patterns.push({ tag, count: groupEntries.length, entries: groupEntries });
|
|
5188
|
+
}
|
|
5189
|
+
}
|
|
5190
|
+
return patterns.sort((a, b) => b.count - a.count);
|
|
5191
|
+
}
|
|
5192
|
+
async function loadBudgetedLearnings(projectPath, options) {
|
|
5193
|
+
const { intent, tokenBudget = 1e3, skill, session, stream } = options;
|
|
5194
|
+
const sortByRecencyAndRelevance = (entries) => {
|
|
5195
|
+
return [...entries].sort((a, b) => {
|
|
5196
|
+
const dateA = parseDateFromEntry(a) ?? "0000-00-00";
|
|
5197
|
+
const dateB = parseDateFromEntry(b) ?? "0000-00-00";
|
|
5198
|
+
const dateCompare = dateB.localeCompare(dateA);
|
|
5199
|
+
if (dateCompare !== 0) return dateCompare;
|
|
5200
|
+
return scoreRelevance(b, intent) - scoreRelevance(a, intent);
|
|
5201
|
+
});
|
|
5202
|
+
};
|
|
5203
|
+
const allEntries = [];
|
|
5204
|
+
if (session) {
|
|
5205
|
+
const sessionResult = await loadRelevantLearnings(projectPath, skill, stream, session);
|
|
5206
|
+
if (sessionResult.ok) {
|
|
5207
|
+
allEntries.push(...sortByRecencyAndRelevance(sessionResult.value));
|
|
5208
|
+
}
|
|
5209
|
+
}
|
|
5210
|
+
const globalResult = await loadRelevantLearnings(projectPath, skill, stream);
|
|
5211
|
+
if (globalResult.ok) {
|
|
5212
|
+
allEntries.push(...sortByRecencyAndRelevance(globalResult.value));
|
|
5213
|
+
}
|
|
5214
|
+
const budgeted = [];
|
|
5215
|
+
let totalTokens = 0;
|
|
5216
|
+
for (const entry of allEntries) {
|
|
5217
|
+
const separator = budgeted.length > 0 ? "\n" : "";
|
|
5218
|
+
const entryCost = estimateTokens(entry + separator);
|
|
5219
|
+
if (totalTokens + entryCost > tokenBudget) break;
|
|
5220
|
+
budgeted.push(entry);
|
|
5221
|
+
totalTokens += entryCost;
|
|
5222
|
+
}
|
|
5223
|
+
return Ok(budgeted);
|
|
5224
|
+
}
|
|
5225
|
+
async function loadRelevantLearnings(projectPath, skillName, stream, session) {
|
|
5082
5226
|
try {
|
|
5083
|
-
const dirResult = await getStateDir(projectPath, stream);
|
|
5227
|
+
const dirResult = await getStateDir(projectPath, stream, session);
|
|
5084
5228
|
if (!dirResult.ok) return dirResult;
|
|
5085
5229
|
const stateDir = dirResult.value;
|
|
5086
|
-
const learningsPath =
|
|
5087
|
-
if (!
|
|
5230
|
+
const learningsPath = path6.join(stateDir, LEARNINGS_FILE);
|
|
5231
|
+
if (!fs9.existsSync(learningsPath)) {
|
|
5088
5232
|
return Ok([]);
|
|
5089
5233
|
}
|
|
5090
|
-
const stats =
|
|
5234
|
+
const stats = fs9.statSync(learningsPath);
|
|
5091
5235
|
const cacheKey = learningsPath;
|
|
5092
5236
|
const cached = learningsCacheMap.get(cacheKey);
|
|
5093
5237
|
let entries;
|
|
5094
5238
|
if (cached && cached.mtimeMs === stats.mtimeMs) {
|
|
5095
5239
|
entries = cached.entries;
|
|
5096
5240
|
} else {
|
|
5097
|
-
const content =
|
|
5241
|
+
const content = fs9.readFileSync(learningsPath, "utf-8");
|
|
5098
5242
|
const lines = content.split("\n");
|
|
5099
5243
|
entries = [];
|
|
5100
5244
|
let currentBlock = [];
|
|
@@ -5130,23 +5274,110 @@ async function loadRelevantLearnings(projectPath, skillName, stream) {
|
|
|
5130
5274
|
);
|
|
5131
5275
|
}
|
|
5132
5276
|
}
|
|
5133
|
-
|
|
5134
|
-
|
|
5277
|
+
async function archiveLearnings(projectPath, entries, stream) {
|
|
5278
|
+
try {
|
|
5279
|
+
const dirResult = await getStateDir(projectPath, stream);
|
|
5280
|
+
if (!dirResult.ok) return dirResult;
|
|
5281
|
+
const stateDir = dirResult.value;
|
|
5282
|
+
const archiveDir = path6.join(stateDir, "learnings-archive");
|
|
5283
|
+
fs9.mkdirSync(archiveDir, { recursive: true });
|
|
5284
|
+
const now = /* @__PURE__ */ new Date();
|
|
5285
|
+
const yearMonth = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, "0")}`;
|
|
5286
|
+
const archivePath = path6.join(archiveDir, `${yearMonth}.md`);
|
|
5287
|
+
const archiveContent = entries.join("\n\n") + "\n";
|
|
5288
|
+
if (fs9.existsSync(archivePath)) {
|
|
5289
|
+
fs9.appendFileSync(archivePath, "\n" + archiveContent);
|
|
5290
|
+
} else {
|
|
5291
|
+
fs9.writeFileSync(archivePath, `# Learnings Archive
|
|
5292
|
+
|
|
5293
|
+
${archiveContent}`);
|
|
5294
|
+
}
|
|
5295
|
+
return Ok(void 0);
|
|
5296
|
+
} catch (error) {
|
|
5297
|
+
return Err(
|
|
5298
|
+
new Error(
|
|
5299
|
+
`Failed to archive learnings: ${error instanceof Error ? error.message : String(error)}`
|
|
5300
|
+
)
|
|
5301
|
+
);
|
|
5302
|
+
}
|
|
5303
|
+
}
|
|
5304
|
+
async function pruneLearnings(projectPath, stream) {
|
|
5135
5305
|
try {
|
|
5136
5306
|
const dirResult = await getStateDir(projectPath, stream);
|
|
5137
5307
|
if (!dirResult.ok) return dirResult;
|
|
5138
5308
|
const stateDir = dirResult.value;
|
|
5139
|
-
const
|
|
5140
|
-
|
|
5309
|
+
const learningsPath = path6.join(stateDir, LEARNINGS_FILE);
|
|
5310
|
+
if (!fs9.existsSync(learningsPath)) {
|
|
5311
|
+
return Ok({ kept: 0, archived: 0, patterns: [] });
|
|
5312
|
+
}
|
|
5313
|
+
const loadResult = await loadRelevantLearnings(projectPath, void 0, stream);
|
|
5314
|
+
if (!loadResult.ok) return loadResult;
|
|
5315
|
+
const allEntries = loadResult.value;
|
|
5316
|
+
if (allEntries.length <= 20) {
|
|
5317
|
+
const cutoffDate = /* @__PURE__ */ new Date();
|
|
5318
|
+
cutoffDate.setDate(cutoffDate.getDate() - 14);
|
|
5319
|
+
const cutoffStr = cutoffDate.toISOString().split("T")[0];
|
|
5320
|
+
const hasOld = allEntries.some((entry) => {
|
|
5321
|
+
const date = parseDateFromEntry(entry);
|
|
5322
|
+
return date !== null && date < cutoffStr;
|
|
5323
|
+
});
|
|
5324
|
+
if (!hasOld) {
|
|
5325
|
+
return Ok({ kept: allEntries.length, archived: 0, patterns: [] });
|
|
5326
|
+
}
|
|
5327
|
+
}
|
|
5328
|
+
const sorted = [...allEntries].sort((a, b) => {
|
|
5329
|
+
const dateA = parseDateFromEntry(a) ?? "0000-00-00";
|
|
5330
|
+
const dateB = parseDateFromEntry(b) ?? "0000-00-00";
|
|
5331
|
+
return dateB.localeCompare(dateA);
|
|
5332
|
+
});
|
|
5333
|
+
const toKeep = sorted.slice(0, 20);
|
|
5334
|
+
const toArchive = sorted.slice(20);
|
|
5335
|
+
const patterns = analyzeLearningPatterns(allEntries);
|
|
5336
|
+
if (toArchive.length > 0) {
|
|
5337
|
+
const archiveResult = await archiveLearnings(projectPath, toArchive, stream);
|
|
5338
|
+
if (!archiveResult.ok) return archiveResult;
|
|
5339
|
+
}
|
|
5340
|
+
const newContent = "# Learnings\n\n" + toKeep.join("\n\n") + "\n";
|
|
5341
|
+
fs9.writeFileSync(learningsPath, newContent);
|
|
5342
|
+
learningsCacheMap.delete(learningsPath);
|
|
5343
|
+
return Ok({
|
|
5344
|
+
kept: toKeep.length,
|
|
5345
|
+
archived: toArchive.length,
|
|
5346
|
+
patterns
|
|
5347
|
+
});
|
|
5348
|
+
} catch (error) {
|
|
5349
|
+
return Err(
|
|
5350
|
+
new Error(
|
|
5351
|
+
`Failed to prune learnings: ${error instanceof Error ? error.message : String(error)}`
|
|
5352
|
+
)
|
|
5353
|
+
);
|
|
5354
|
+
}
|
|
5355
|
+
}
|
|
5356
|
+
|
|
5357
|
+
// src/state/failures.ts
|
|
5358
|
+
import * as fs10 from "fs";
|
|
5359
|
+
import * as path7 from "path";
|
|
5360
|
+
var failuresCacheMap = /* @__PURE__ */ new Map();
|
|
5361
|
+
function clearFailuresCache() {
|
|
5362
|
+
failuresCacheMap.clear();
|
|
5363
|
+
}
|
|
5364
|
+
var FAILURE_LINE_REGEX = /^- \*\*(\d{4}-\d{2}-\d{2}) \[skill:([^\]]+)\] \[type:([^\]]+)\]:\*\* (.+)$/;
|
|
5365
|
+
async function appendFailure(projectPath, description, skillName, type, stream, session) {
|
|
5366
|
+
try {
|
|
5367
|
+
const dirResult = await getStateDir(projectPath, stream, session);
|
|
5368
|
+
if (!dirResult.ok) return dirResult;
|
|
5369
|
+
const stateDir = dirResult.value;
|
|
5370
|
+
const failuresPath = path7.join(stateDir, FAILURES_FILE);
|
|
5371
|
+
fs10.mkdirSync(stateDir, { recursive: true });
|
|
5141
5372
|
const timestamp = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
|
|
5142
5373
|
const entry = `
|
|
5143
5374
|
- **${timestamp} [skill:${skillName}] [type:${type}]:** ${description}
|
|
5144
5375
|
`;
|
|
5145
|
-
if (!
|
|
5146
|
-
|
|
5376
|
+
if (!fs10.existsSync(failuresPath)) {
|
|
5377
|
+
fs10.writeFileSync(failuresPath, `# Failures
|
|
5147
5378
|
${entry}`);
|
|
5148
5379
|
} else {
|
|
5149
|
-
|
|
5380
|
+
fs10.appendFileSync(failuresPath, entry);
|
|
5150
5381
|
}
|
|
5151
5382
|
failuresCacheMap.delete(failuresPath);
|
|
5152
5383
|
return Ok(void 0);
|
|
@@ -5158,22 +5389,22 @@ ${entry}`);
|
|
|
5158
5389
|
);
|
|
5159
5390
|
}
|
|
5160
5391
|
}
|
|
5161
|
-
async function loadFailures(projectPath, stream) {
|
|
5392
|
+
async function loadFailures(projectPath, stream, session) {
|
|
5162
5393
|
try {
|
|
5163
|
-
const dirResult = await getStateDir(projectPath, stream);
|
|
5394
|
+
const dirResult = await getStateDir(projectPath, stream, session);
|
|
5164
5395
|
if (!dirResult.ok) return dirResult;
|
|
5165
5396
|
const stateDir = dirResult.value;
|
|
5166
|
-
const failuresPath =
|
|
5167
|
-
if (!
|
|
5397
|
+
const failuresPath = path7.join(stateDir, FAILURES_FILE);
|
|
5398
|
+
if (!fs10.existsSync(failuresPath)) {
|
|
5168
5399
|
return Ok([]);
|
|
5169
5400
|
}
|
|
5170
|
-
const stats =
|
|
5401
|
+
const stats = fs10.statSync(failuresPath);
|
|
5171
5402
|
const cacheKey = failuresPath;
|
|
5172
5403
|
const cached = failuresCacheMap.get(cacheKey);
|
|
5173
5404
|
if (cached && cached.mtimeMs === stats.mtimeMs) {
|
|
5174
5405
|
return Ok(cached.entries);
|
|
5175
5406
|
}
|
|
5176
|
-
const content =
|
|
5407
|
+
const content = fs10.readFileSync(failuresPath, "utf-8");
|
|
5177
5408
|
const entries = [];
|
|
5178
5409
|
for (const line of content.split("\n")) {
|
|
5179
5410
|
const match = line.match(FAILURE_LINE_REGEX);
|
|
@@ -5197,25 +5428,25 @@ async function loadFailures(projectPath, stream) {
|
|
|
5197
5428
|
);
|
|
5198
5429
|
}
|
|
5199
5430
|
}
|
|
5200
|
-
async function archiveFailures(projectPath, stream) {
|
|
5431
|
+
async function archiveFailures(projectPath, stream, session) {
|
|
5201
5432
|
try {
|
|
5202
|
-
const dirResult = await getStateDir(projectPath, stream);
|
|
5433
|
+
const dirResult = await getStateDir(projectPath, stream, session);
|
|
5203
5434
|
if (!dirResult.ok) return dirResult;
|
|
5204
5435
|
const stateDir = dirResult.value;
|
|
5205
|
-
const failuresPath =
|
|
5206
|
-
if (!
|
|
5436
|
+
const failuresPath = path7.join(stateDir, FAILURES_FILE);
|
|
5437
|
+
if (!fs10.existsSync(failuresPath)) {
|
|
5207
5438
|
return Ok(void 0);
|
|
5208
5439
|
}
|
|
5209
|
-
const archiveDir =
|
|
5210
|
-
|
|
5440
|
+
const archiveDir = path7.join(stateDir, "archive");
|
|
5441
|
+
fs10.mkdirSync(archiveDir, { recursive: true });
|
|
5211
5442
|
const date = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
|
|
5212
5443
|
let archiveName = `failures-${date}.md`;
|
|
5213
5444
|
let counter = 2;
|
|
5214
|
-
while (
|
|
5445
|
+
while (fs10.existsSync(path7.join(archiveDir, archiveName))) {
|
|
5215
5446
|
archiveName = `failures-${date}-${counter}.md`;
|
|
5216
5447
|
counter++;
|
|
5217
5448
|
}
|
|
5218
|
-
|
|
5449
|
+
fs10.renameSync(failuresPath, path7.join(archiveDir, archiveName));
|
|
5219
5450
|
failuresCacheMap.delete(failuresPath);
|
|
5220
5451
|
return Ok(void 0);
|
|
5221
5452
|
} catch (error) {
|
|
@@ -5226,14 +5457,18 @@ async function archiveFailures(projectPath, stream) {
|
|
|
5226
5457
|
);
|
|
5227
5458
|
}
|
|
5228
5459
|
}
|
|
5229
|
-
|
|
5460
|
+
|
|
5461
|
+
// src/state/handoff.ts
|
|
5462
|
+
import * as fs11 from "fs";
|
|
5463
|
+
import * as path8 from "path";
|
|
5464
|
+
async function saveHandoff(projectPath, handoff, stream, session) {
|
|
5230
5465
|
try {
|
|
5231
|
-
const dirResult = await getStateDir(projectPath, stream);
|
|
5466
|
+
const dirResult = await getStateDir(projectPath, stream, session);
|
|
5232
5467
|
if (!dirResult.ok) return dirResult;
|
|
5233
5468
|
const stateDir = dirResult.value;
|
|
5234
|
-
const handoffPath =
|
|
5235
|
-
|
|
5236
|
-
|
|
5469
|
+
const handoffPath = path8.join(stateDir, HANDOFF_FILE);
|
|
5470
|
+
fs11.mkdirSync(stateDir, { recursive: true });
|
|
5471
|
+
fs11.writeFileSync(handoffPath, JSON.stringify(handoff, null, 2));
|
|
5237
5472
|
return Ok(void 0);
|
|
5238
5473
|
} catch (error) {
|
|
5239
5474
|
return Err(
|
|
@@ -5241,16 +5476,16 @@ async function saveHandoff(projectPath, handoff, stream) {
|
|
|
5241
5476
|
);
|
|
5242
5477
|
}
|
|
5243
5478
|
}
|
|
5244
|
-
async function loadHandoff(projectPath, stream) {
|
|
5479
|
+
async function loadHandoff(projectPath, stream, session) {
|
|
5245
5480
|
try {
|
|
5246
|
-
const dirResult = await getStateDir(projectPath, stream);
|
|
5481
|
+
const dirResult = await getStateDir(projectPath, stream, session);
|
|
5247
5482
|
if (!dirResult.ok) return dirResult;
|
|
5248
5483
|
const stateDir = dirResult.value;
|
|
5249
|
-
const handoffPath =
|
|
5250
|
-
if (!
|
|
5484
|
+
const handoffPath = path8.join(stateDir, HANDOFF_FILE);
|
|
5485
|
+
if (!fs11.existsSync(handoffPath)) {
|
|
5251
5486
|
return Ok(null);
|
|
5252
5487
|
}
|
|
5253
|
-
const raw =
|
|
5488
|
+
const raw = fs11.readFileSync(handoffPath, "utf-8");
|
|
5254
5489
|
const parsed = JSON.parse(raw);
|
|
5255
5490
|
const result = HandoffSchema.safeParse(parsed);
|
|
5256
5491
|
if (!result.success) {
|
|
@@ -5263,73 +5498,82 @@ async function loadHandoff(projectPath, stream) {
|
|
|
5263
5498
|
);
|
|
5264
5499
|
}
|
|
5265
5500
|
}
|
|
5501
|
+
|
|
5502
|
+
// src/state/mechanical-gate.ts
|
|
5503
|
+
import * as fs12 from "fs";
|
|
5504
|
+
import * as path9 from "path";
|
|
5505
|
+
import { execSync as execSync2 } from "child_process";
|
|
5506
|
+
var SAFE_GATE_COMMAND = /^(?:npm|pnpm|yarn)\s+(?:test|run\s+[\w.-]+|run-script\s+[\w.-]+)$|^go\s+(?:test|build|vet|fmt)\s+[\w./ -]+$|^(?:python|python3)\s+-m\s+[\w.-]+$|^make\s+[\w.-]+$|^cargo\s+(?:test|build|check|clippy)(?:\s+[\w./ -]+)?$|^(?:gradle|mvn)\s+[\w:.-]+$/;
|
|
5507
|
+
function loadChecksFromConfig(gateConfigPath) {
|
|
5508
|
+
if (!fs12.existsSync(gateConfigPath)) return [];
|
|
5509
|
+
const raw = JSON.parse(fs12.readFileSync(gateConfigPath, "utf-8"));
|
|
5510
|
+
const config = GateConfigSchema.safeParse(raw);
|
|
5511
|
+
if (config.success && config.data.checks) return config.data.checks;
|
|
5512
|
+
return [];
|
|
5513
|
+
}
|
|
5514
|
+
function discoverChecksFromProject(projectPath) {
|
|
5515
|
+
const checks = [];
|
|
5516
|
+
const packageJsonPath = path9.join(projectPath, "package.json");
|
|
5517
|
+
if (fs12.existsSync(packageJsonPath)) {
|
|
5518
|
+
const pkg = JSON.parse(fs12.readFileSync(packageJsonPath, "utf-8"));
|
|
5519
|
+
const scripts = pkg.scripts || {};
|
|
5520
|
+
if (scripts.test) checks.push({ name: "test", command: "npm test" });
|
|
5521
|
+
if (scripts.lint) checks.push({ name: "lint", command: "npm run lint" });
|
|
5522
|
+
if (scripts.typecheck) checks.push({ name: "typecheck", command: "npm run typecheck" });
|
|
5523
|
+
if (scripts.build) checks.push({ name: "build", command: "npm run build" });
|
|
5524
|
+
}
|
|
5525
|
+
if (fs12.existsSync(path9.join(projectPath, "go.mod"))) {
|
|
5526
|
+
checks.push({ name: "test", command: "go test ./..." });
|
|
5527
|
+
checks.push({ name: "build", command: "go build ./..." });
|
|
5528
|
+
}
|
|
5529
|
+
if (fs12.existsSync(path9.join(projectPath, "pyproject.toml")) || fs12.existsSync(path9.join(projectPath, "setup.py"))) {
|
|
5530
|
+
checks.push({ name: "test", command: "python -m pytest" });
|
|
5531
|
+
}
|
|
5532
|
+
return checks;
|
|
5533
|
+
}
|
|
5534
|
+
function executeCheck(check, projectPath) {
|
|
5535
|
+
if (!SAFE_GATE_COMMAND.test(check.command)) {
|
|
5536
|
+
return {
|
|
5537
|
+
name: check.name,
|
|
5538
|
+
passed: false,
|
|
5539
|
+
command: check.command,
|
|
5540
|
+
output: `Blocked: command does not match safe gate pattern. Allowed prefixes: npm, pnpm, yarn, go, python, python3, make, cargo, gradle, mvn`,
|
|
5541
|
+
duration: 0
|
|
5542
|
+
};
|
|
5543
|
+
}
|
|
5544
|
+
const start = Date.now();
|
|
5545
|
+
try {
|
|
5546
|
+
execSync2(check.command, {
|
|
5547
|
+
cwd: projectPath,
|
|
5548
|
+
stdio: "pipe",
|
|
5549
|
+
timeout: 12e4
|
|
5550
|
+
});
|
|
5551
|
+
return {
|
|
5552
|
+
name: check.name,
|
|
5553
|
+
passed: true,
|
|
5554
|
+
command: check.command,
|
|
5555
|
+
duration: Date.now() - start
|
|
5556
|
+
};
|
|
5557
|
+
} catch (error) {
|
|
5558
|
+
const output = error instanceof Error ? error.stderr?.toString() || error.message : String(error);
|
|
5559
|
+
return {
|
|
5560
|
+
name: check.name,
|
|
5561
|
+
passed: false,
|
|
5562
|
+
command: check.command,
|
|
5563
|
+
output: output.slice(0, 2e3),
|
|
5564
|
+
duration: Date.now() - start
|
|
5565
|
+
};
|
|
5566
|
+
}
|
|
5567
|
+
}
|
|
5266
5568
|
async function runMechanicalGate(projectPath) {
|
|
5267
|
-
const harnessDir =
|
|
5268
|
-
const gateConfigPath =
|
|
5569
|
+
const harnessDir = path9.join(projectPath, HARNESS_DIR);
|
|
5570
|
+
const gateConfigPath = path9.join(harnessDir, GATE_CONFIG_FILE);
|
|
5269
5571
|
try {
|
|
5270
|
-
let checks =
|
|
5271
|
-
if (fs6.existsSync(gateConfigPath)) {
|
|
5272
|
-
const raw = JSON.parse(fs6.readFileSync(gateConfigPath, "utf-8"));
|
|
5273
|
-
const config = GateConfigSchema.safeParse(raw);
|
|
5274
|
-
if (config.success && config.data.checks) {
|
|
5275
|
-
checks = config.data.checks;
|
|
5276
|
-
}
|
|
5277
|
-
}
|
|
5572
|
+
let checks = loadChecksFromConfig(gateConfigPath);
|
|
5278
5573
|
if (checks.length === 0) {
|
|
5279
|
-
|
|
5280
|
-
if (fs6.existsSync(packageJsonPath)) {
|
|
5281
|
-
const pkg = JSON.parse(fs6.readFileSync(packageJsonPath, "utf-8"));
|
|
5282
|
-
const scripts = pkg.scripts || {};
|
|
5283
|
-
if (scripts.test) checks.push({ name: "test", command: "npm test" });
|
|
5284
|
-
if (scripts.lint) checks.push({ name: "lint", command: "npm run lint" });
|
|
5285
|
-
if (scripts.typecheck) checks.push({ name: "typecheck", command: "npm run typecheck" });
|
|
5286
|
-
if (scripts.build) checks.push({ name: "build", command: "npm run build" });
|
|
5287
|
-
}
|
|
5288
|
-
if (fs6.existsSync(path3.join(projectPath, "go.mod"))) {
|
|
5289
|
-
checks.push({ name: "test", command: "go test ./..." });
|
|
5290
|
-
checks.push({ name: "build", command: "go build ./..." });
|
|
5291
|
-
}
|
|
5292
|
-
if (fs6.existsSync(path3.join(projectPath, "pyproject.toml")) || fs6.existsSync(path3.join(projectPath, "setup.py"))) {
|
|
5293
|
-
checks.push({ name: "test", command: "python -m pytest" });
|
|
5294
|
-
}
|
|
5295
|
-
}
|
|
5296
|
-
const results = [];
|
|
5297
|
-
const SAFE_GATE_COMMAND = /^(?:npm|pnpm|yarn)\s+(?:test|run\s+[\w.-]+|run-script\s+[\w.-]+)$|^go\s+(?:test|build|vet|fmt)\s+[\w./ -]+$|^(?:python|python3)\s+-m\s+[\w.-]+$|^make\s+[\w.-]+$|^cargo\s+(?:test|build|check|clippy)(?:\s+[\w./ -]+)?$|^(?:gradle|mvn)\s+[\w:.-]+$/;
|
|
5298
|
-
for (const check of checks) {
|
|
5299
|
-
if (!SAFE_GATE_COMMAND.test(check.command)) {
|
|
5300
|
-
results.push({
|
|
5301
|
-
name: check.name,
|
|
5302
|
-
passed: false,
|
|
5303
|
-
command: check.command,
|
|
5304
|
-
output: `Blocked: command does not match safe gate pattern. Allowed prefixes: npm, npx, pnpm, yarn, go, python, python3, make, cargo, gradle, mvn`,
|
|
5305
|
-
duration: 0
|
|
5306
|
-
});
|
|
5307
|
-
continue;
|
|
5308
|
-
}
|
|
5309
|
-
const start = Date.now();
|
|
5310
|
-
try {
|
|
5311
|
-
execSync2(check.command, {
|
|
5312
|
-
cwd: projectPath,
|
|
5313
|
-
stdio: "pipe",
|
|
5314
|
-
timeout: 12e4
|
|
5315
|
-
});
|
|
5316
|
-
results.push({
|
|
5317
|
-
name: check.name,
|
|
5318
|
-
passed: true,
|
|
5319
|
-
command: check.command,
|
|
5320
|
-
duration: Date.now() - start
|
|
5321
|
-
});
|
|
5322
|
-
} catch (error) {
|
|
5323
|
-
const output = error instanceof Error ? error.stderr?.toString() || error.message : String(error);
|
|
5324
|
-
results.push({
|
|
5325
|
-
name: check.name,
|
|
5326
|
-
passed: false,
|
|
5327
|
-
command: check.command,
|
|
5328
|
-
output: output.slice(0, 2e3),
|
|
5329
|
-
duration: Date.now() - start
|
|
5330
|
-
});
|
|
5331
|
-
}
|
|
5574
|
+
checks = discoverChecksFromProject(projectPath);
|
|
5332
5575
|
}
|
|
5576
|
+
const results = checks.map((check) => executeCheck(check, projectPath));
|
|
5333
5577
|
return Ok({
|
|
5334
5578
|
passed: results.length === 0 || results.every((r) => r.passed),
|
|
5335
5579
|
checks: results
|
|
@@ -5343,6 +5587,96 @@ async function runMechanicalGate(projectPath) {
|
|
|
5343
5587
|
}
|
|
5344
5588
|
}
|
|
5345
5589
|
|
|
5590
|
+
// src/state/session-summary.ts
|
|
5591
|
+
import * as fs13 from "fs";
|
|
5592
|
+
import * as path10 from "path";
|
|
5593
|
+
function formatSummary(data) {
|
|
5594
|
+
const lines = [
|
|
5595
|
+
"## Session Summary",
|
|
5596
|
+
"",
|
|
5597
|
+
`**Session:** ${data.session}`,
|
|
5598
|
+
`**Last active:** ${data.lastActive}`,
|
|
5599
|
+
`**Skill:** ${data.skill}`
|
|
5600
|
+
];
|
|
5601
|
+
if (data.phase) {
|
|
5602
|
+
lines.push(`**Phase:** ${data.phase}`);
|
|
5603
|
+
}
|
|
5604
|
+
lines.push(`**Status:** ${data.status}`);
|
|
5605
|
+
if (data.spec) {
|
|
5606
|
+
lines.push(`**Spec:** ${data.spec}`);
|
|
5607
|
+
}
|
|
5608
|
+
if (data.plan) {
|
|
5609
|
+
lines.push(`**Plan:** ${data.plan}`);
|
|
5610
|
+
}
|
|
5611
|
+
lines.push(`**Key context:** ${data.keyContext}`);
|
|
5612
|
+
lines.push(`**Next step:** ${data.nextStep}`);
|
|
5613
|
+
lines.push("");
|
|
5614
|
+
return lines.join("\n");
|
|
5615
|
+
}
|
|
5616
|
+
function deriveIndexDescription(data) {
|
|
5617
|
+
const skillShort = data.skill.replace("harness-", "");
|
|
5618
|
+
const parts = [skillShort];
|
|
5619
|
+
if (data.phase) {
|
|
5620
|
+
parts.push(`phase ${data.phase}`);
|
|
5621
|
+
}
|
|
5622
|
+
parts.push(data.status.toLowerCase());
|
|
5623
|
+
return parts.join(", ");
|
|
5624
|
+
}
|
|
5625
|
+
function writeSessionSummary(projectPath, sessionSlug, data) {
|
|
5626
|
+
try {
|
|
5627
|
+
const dirResult = resolveSessionDir(projectPath, sessionSlug, { create: true });
|
|
5628
|
+
if (!dirResult.ok) return dirResult;
|
|
5629
|
+
const sessionDir = dirResult.value;
|
|
5630
|
+
const summaryPath = path10.join(sessionDir, SUMMARY_FILE);
|
|
5631
|
+
const content = formatSummary(data);
|
|
5632
|
+
fs13.writeFileSync(summaryPath, content);
|
|
5633
|
+
const description = deriveIndexDescription(data);
|
|
5634
|
+
updateSessionIndex(projectPath, sessionSlug, description);
|
|
5635
|
+
return Ok(void 0);
|
|
5636
|
+
} catch (error) {
|
|
5637
|
+
return Err(
|
|
5638
|
+
new Error(
|
|
5639
|
+
`Failed to write session summary: ${error instanceof Error ? error.message : String(error)}`
|
|
5640
|
+
)
|
|
5641
|
+
);
|
|
5642
|
+
}
|
|
5643
|
+
}
|
|
5644
|
+
function loadSessionSummary(projectPath, sessionSlug) {
|
|
5645
|
+
try {
|
|
5646
|
+
const dirResult = resolveSessionDir(projectPath, sessionSlug);
|
|
5647
|
+
if (!dirResult.ok) return dirResult;
|
|
5648
|
+
const sessionDir = dirResult.value;
|
|
5649
|
+
const summaryPath = path10.join(sessionDir, SUMMARY_FILE);
|
|
5650
|
+
if (!fs13.existsSync(summaryPath)) {
|
|
5651
|
+
return Ok(null);
|
|
5652
|
+
}
|
|
5653
|
+
const content = fs13.readFileSync(summaryPath, "utf-8");
|
|
5654
|
+
return Ok(content);
|
|
5655
|
+
} catch (error) {
|
|
5656
|
+
return Err(
|
|
5657
|
+
new Error(
|
|
5658
|
+
`Failed to load session summary: ${error instanceof Error ? error.message : String(error)}`
|
|
5659
|
+
)
|
|
5660
|
+
);
|
|
5661
|
+
}
|
|
5662
|
+
}
|
|
5663
|
+
function listActiveSessions(projectPath) {
|
|
5664
|
+
try {
|
|
5665
|
+
const indexPath2 = path10.join(projectPath, HARNESS_DIR, SESSIONS_DIR, SESSION_INDEX_FILE);
|
|
5666
|
+
if (!fs13.existsSync(indexPath2)) {
|
|
5667
|
+
return Ok(null);
|
|
5668
|
+
}
|
|
5669
|
+
const content = fs13.readFileSync(indexPath2, "utf-8");
|
|
5670
|
+
return Ok(content);
|
|
5671
|
+
} catch (error) {
|
|
5672
|
+
return Err(
|
|
5673
|
+
new Error(
|
|
5674
|
+
`Failed to list active sessions: ${error instanceof Error ? error.message : String(error)}`
|
|
5675
|
+
)
|
|
5676
|
+
);
|
|
5677
|
+
}
|
|
5678
|
+
}
|
|
5679
|
+
|
|
5346
5680
|
// src/workflow/runner.ts
|
|
5347
5681
|
async function executeWorkflow(workflow, executor) {
|
|
5348
5682
|
const stepResults = [];
|
|
@@ -5492,7 +5826,7 @@ async function runMultiTurnPipeline(initialContext, turnExecutor, options) {
|
|
|
5492
5826
|
}
|
|
5493
5827
|
|
|
5494
5828
|
// src/security/scanner.ts
|
|
5495
|
-
import * as
|
|
5829
|
+
import * as fs15 from "fs/promises";
|
|
5496
5830
|
|
|
5497
5831
|
// src/security/rules/registry.ts
|
|
5498
5832
|
var RuleRegistry = class {
|
|
@@ -5579,15 +5913,15 @@ function resolveRuleSeverity(ruleId, defaultSeverity, overrides, strict) {
|
|
|
5579
5913
|
}
|
|
5580
5914
|
|
|
5581
5915
|
// src/security/stack-detector.ts
|
|
5582
|
-
import * as
|
|
5583
|
-
import * as
|
|
5916
|
+
import * as fs14 from "fs";
|
|
5917
|
+
import * as path11 from "path";
|
|
5584
5918
|
function detectStack(projectRoot) {
|
|
5585
5919
|
const stacks = [];
|
|
5586
|
-
const pkgJsonPath =
|
|
5587
|
-
if (
|
|
5920
|
+
const pkgJsonPath = path11.join(projectRoot, "package.json");
|
|
5921
|
+
if (fs14.existsSync(pkgJsonPath)) {
|
|
5588
5922
|
stacks.push("node");
|
|
5589
5923
|
try {
|
|
5590
|
-
const pkgJson = JSON.parse(
|
|
5924
|
+
const pkgJson = JSON.parse(fs14.readFileSync(pkgJsonPath, "utf-8"));
|
|
5591
5925
|
const allDeps = {
|
|
5592
5926
|
...pkgJson.dependencies,
|
|
5593
5927
|
...pkgJson.devDependencies
|
|
@@ -5602,13 +5936,13 @@ function detectStack(projectRoot) {
|
|
|
5602
5936
|
} catch {
|
|
5603
5937
|
}
|
|
5604
5938
|
}
|
|
5605
|
-
const goModPath =
|
|
5606
|
-
if (
|
|
5939
|
+
const goModPath = path11.join(projectRoot, "go.mod");
|
|
5940
|
+
if (fs14.existsSync(goModPath)) {
|
|
5607
5941
|
stacks.push("go");
|
|
5608
5942
|
}
|
|
5609
|
-
const requirementsPath =
|
|
5610
|
-
const pyprojectPath =
|
|
5611
|
-
if (
|
|
5943
|
+
const requirementsPath = path11.join(projectRoot, "requirements.txt");
|
|
5944
|
+
const pyprojectPath = path11.join(projectRoot, "pyproject.toml");
|
|
5945
|
+
if (fs14.existsSync(requirementsPath) || fs14.existsSync(pyprojectPath)) {
|
|
5612
5946
|
stacks.push("python");
|
|
5613
5947
|
}
|
|
5614
5948
|
return stacks;
|
|
@@ -6035,7 +6369,7 @@ var SecurityScanner = class {
|
|
|
6035
6369
|
}
|
|
6036
6370
|
async scanFile(filePath) {
|
|
6037
6371
|
if (!this.config.enabled) return [];
|
|
6038
|
-
const content = await
|
|
6372
|
+
const content = await fs15.readFile(filePath, "utf-8");
|
|
6039
6373
|
return this.scanContent(content, filePath, 1);
|
|
6040
6374
|
}
|
|
6041
6375
|
async scanFiles(filePaths) {
|
|
@@ -6060,7 +6394,7 @@ var SecurityScanner = class {
|
|
|
6060
6394
|
};
|
|
6061
6395
|
|
|
6062
6396
|
// src/ci/check-orchestrator.ts
|
|
6063
|
-
import * as
|
|
6397
|
+
import * as path12 from "path";
|
|
6064
6398
|
var ALL_CHECKS = [
|
|
6065
6399
|
"validate",
|
|
6066
6400
|
"deps",
|
|
@@ -6077,7 +6411,7 @@ async function runSingleCheck(name, projectRoot, config) {
|
|
|
6077
6411
|
try {
|
|
6078
6412
|
switch (name) {
|
|
6079
6413
|
case "validate": {
|
|
6080
|
-
const agentsPath =
|
|
6414
|
+
const agentsPath = path12.join(projectRoot, config.agentsMapPath ?? "AGENTS.md");
|
|
6081
6415
|
const result = await validateAgentsMap(agentsPath);
|
|
6082
6416
|
if (!result.ok) {
|
|
6083
6417
|
issues.push({ severity: "error", message: result.error.message });
|
|
@@ -6132,7 +6466,7 @@ async function runSingleCheck(name, projectRoot, config) {
|
|
|
6132
6466
|
break;
|
|
6133
6467
|
}
|
|
6134
6468
|
case "docs": {
|
|
6135
|
-
const docsDir =
|
|
6469
|
+
const docsDir = path12.join(projectRoot, config.docsDir ?? "docs");
|
|
6136
6470
|
const entropyConfig = config.entropy || {};
|
|
6137
6471
|
const result = await checkDocCoverage("project", {
|
|
6138
6472
|
docsDir,
|
|
@@ -6370,7 +6704,7 @@ async function runCIChecks(input) {
|
|
|
6370
6704
|
}
|
|
6371
6705
|
|
|
6372
6706
|
// src/review/mechanical-checks.ts
|
|
6373
|
-
import * as
|
|
6707
|
+
import * as path13 from "path";
|
|
6374
6708
|
async function runMechanicalChecks(options) {
|
|
6375
6709
|
const { projectRoot, config, skip = [], changedFiles } = options;
|
|
6376
6710
|
const findings = [];
|
|
@@ -6382,7 +6716,7 @@ async function runMechanicalChecks(options) {
|
|
|
6382
6716
|
};
|
|
6383
6717
|
if (!skip.includes("validate")) {
|
|
6384
6718
|
try {
|
|
6385
|
-
const agentsPath =
|
|
6719
|
+
const agentsPath = path13.join(projectRoot, config.agentsMapPath ?? "AGENTS.md");
|
|
6386
6720
|
const result = await validateAgentsMap(agentsPath);
|
|
6387
6721
|
if (!result.ok) {
|
|
6388
6722
|
statuses.validate = "fail";
|
|
@@ -6419,7 +6753,7 @@ async function runMechanicalChecks(options) {
|
|
|
6419
6753
|
statuses.validate = "fail";
|
|
6420
6754
|
findings.push({
|
|
6421
6755
|
tool: "validate",
|
|
6422
|
-
file:
|
|
6756
|
+
file: path13.join(projectRoot, "AGENTS.md"),
|
|
6423
6757
|
message: err instanceof Error ? err.message : String(err),
|
|
6424
6758
|
severity: "error"
|
|
6425
6759
|
});
|
|
@@ -6483,7 +6817,7 @@ async function runMechanicalChecks(options) {
|
|
|
6483
6817
|
(async () => {
|
|
6484
6818
|
const localFindings = [];
|
|
6485
6819
|
try {
|
|
6486
|
-
const docsDir =
|
|
6820
|
+
const docsDir = path13.join(projectRoot, config.docsDir ?? "docs");
|
|
6487
6821
|
const result = await checkDocCoverage("project", { docsDir });
|
|
6488
6822
|
if (!result.ok) {
|
|
6489
6823
|
statuses["check-docs"] = "warn";
|
|
@@ -6510,7 +6844,7 @@ async function runMechanicalChecks(options) {
|
|
|
6510
6844
|
statuses["check-docs"] = "warn";
|
|
6511
6845
|
localFindings.push({
|
|
6512
6846
|
tool: "check-docs",
|
|
6513
|
-
file:
|
|
6847
|
+
file: path13.join(projectRoot, "docs"),
|
|
6514
6848
|
message: err instanceof Error ? err.message : String(err),
|
|
6515
6849
|
severity: "warning"
|
|
6516
6850
|
});
|
|
@@ -6658,7 +6992,7 @@ function detectChangeType(commitMessage, diff2) {
|
|
|
6658
6992
|
}
|
|
6659
6993
|
|
|
6660
6994
|
// src/review/context-scoper.ts
|
|
6661
|
-
import * as
|
|
6995
|
+
import * as path14 from "path";
|
|
6662
6996
|
var ALL_DOMAINS = ["compliance", "bug", "security", "architecture"];
|
|
6663
6997
|
var SECURITY_PATTERNS = /auth|crypto|password|secret|token|session|cookie|hash|encrypt|decrypt|sql|shell|exec|eval/i;
|
|
6664
6998
|
function computeContextBudget(diffLines) {
|
|
@@ -6666,18 +7000,18 @@ function computeContextBudget(diffLines) {
|
|
|
6666
7000
|
return diffLines;
|
|
6667
7001
|
}
|
|
6668
7002
|
function isWithinProject(absPath, projectRoot) {
|
|
6669
|
-
const resolvedRoot =
|
|
6670
|
-
const resolvedPath =
|
|
6671
|
-
return resolvedPath.startsWith(resolvedRoot) || resolvedPath ===
|
|
7003
|
+
const resolvedRoot = path14.resolve(projectRoot) + path14.sep;
|
|
7004
|
+
const resolvedPath = path14.resolve(absPath);
|
|
7005
|
+
return resolvedPath.startsWith(resolvedRoot) || resolvedPath === path14.resolve(projectRoot);
|
|
6672
7006
|
}
|
|
6673
7007
|
async function readContextFile(projectRoot, filePath, reason) {
|
|
6674
|
-
const absPath =
|
|
7008
|
+
const absPath = path14.isAbsolute(filePath) ? filePath : path14.join(projectRoot, filePath);
|
|
6675
7009
|
if (!isWithinProject(absPath, projectRoot)) return null;
|
|
6676
7010
|
const result = await readFileContent(absPath);
|
|
6677
7011
|
if (!result.ok) return null;
|
|
6678
7012
|
const content = result.value;
|
|
6679
7013
|
const lines = content.split("\n").length;
|
|
6680
|
-
const relPath =
|
|
7014
|
+
const relPath = path14.isAbsolute(filePath) ? path14.relative(projectRoot, filePath) : filePath;
|
|
6681
7015
|
return { path: relPath, content, reason, lines };
|
|
6682
7016
|
}
|
|
6683
7017
|
function extractImportSources(content) {
|
|
@@ -6692,18 +7026,18 @@ function extractImportSources(content) {
|
|
|
6692
7026
|
}
|
|
6693
7027
|
async function resolveImportPath(projectRoot, fromFile, importSource) {
|
|
6694
7028
|
if (!importSource.startsWith(".")) return null;
|
|
6695
|
-
const fromDir =
|
|
6696
|
-
const basePath =
|
|
7029
|
+
const fromDir = path14.dirname(path14.join(projectRoot, fromFile));
|
|
7030
|
+
const basePath = path14.resolve(fromDir, importSource);
|
|
6697
7031
|
if (!isWithinProject(basePath, projectRoot)) return null;
|
|
6698
|
-
const relBase =
|
|
7032
|
+
const relBase = path14.relative(projectRoot, basePath);
|
|
6699
7033
|
const candidates = [
|
|
6700
7034
|
relBase + ".ts",
|
|
6701
7035
|
relBase + ".tsx",
|
|
6702
7036
|
relBase + ".mts",
|
|
6703
|
-
|
|
7037
|
+
path14.join(relBase, "index.ts")
|
|
6704
7038
|
];
|
|
6705
7039
|
for (const candidate of candidates) {
|
|
6706
|
-
const absCandidate =
|
|
7040
|
+
const absCandidate = path14.join(projectRoot, candidate);
|
|
6707
7041
|
if (await fileExists(absCandidate)) {
|
|
6708
7042
|
return candidate;
|
|
6709
7043
|
}
|
|
@@ -6711,10 +7045,10 @@ async function resolveImportPath(projectRoot, fromFile, importSource) {
|
|
|
6711
7045
|
return null;
|
|
6712
7046
|
}
|
|
6713
7047
|
async function findTestFiles(projectRoot, sourceFile) {
|
|
6714
|
-
const baseName =
|
|
7048
|
+
const baseName = path14.basename(sourceFile, path14.extname(sourceFile));
|
|
6715
7049
|
const pattern = `**/${baseName}.{test,spec}.{ts,tsx,mts}`;
|
|
6716
7050
|
const results = await findFiles(pattern, projectRoot);
|
|
6717
|
-
return results.map((f) =>
|
|
7051
|
+
return results.map((f) => path14.relative(projectRoot, f));
|
|
6718
7052
|
}
|
|
6719
7053
|
async function gatherImportContext(projectRoot, changedFiles, budget) {
|
|
6720
7054
|
const contextFiles = [];
|
|
@@ -7502,7 +7836,7 @@ async function fanOutReview(options) {
|
|
|
7502
7836
|
}
|
|
7503
7837
|
|
|
7504
7838
|
// src/review/validate-findings.ts
|
|
7505
|
-
import * as
|
|
7839
|
+
import * as path15 from "path";
|
|
7506
7840
|
var DOWNGRADE_MAP = {
|
|
7507
7841
|
critical: "important",
|
|
7508
7842
|
important: "suggestion",
|
|
@@ -7523,7 +7857,7 @@ function normalizePath(filePath, projectRoot) {
|
|
|
7523
7857
|
let normalized = filePath;
|
|
7524
7858
|
normalized = normalized.replace(/\\/g, "/");
|
|
7525
7859
|
const normalizedRoot = projectRoot.replace(/\\/g, "/");
|
|
7526
|
-
if (
|
|
7860
|
+
if (path15.isAbsolute(normalized)) {
|
|
7527
7861
|
const root = normalizedRoot.endsWith("/") ? normalizedRoot : normalizedRoot + "/";
|
|
7528
7862
|
if (normalized.startsWith(root)) {
|
|
7529
7863
|
normalized = normalized.slice(root.length);
|
|
@@ -7548,12 +7882,12 @@ function followImportChain(fromFile, fileContents, maxDepth = 2) {
|
|
|
7548
7882
|
while ((match = importRegex.exec(content)) !== null) {
|
|
7549
7883
|
const importPath = match[1];
|
|
7550
7884
|
if (!importPath.startsWith(".")) continue;
|
|
7551
|
-
const dir =
|
|
7552
|
-
let resolved =
|
|
7885
|
+
const dir = path15.dirname(current.file);
|
|
7886
|
+
let resolved = path15.join(dir, importPath).replace(/\\/g, "/");
|
|
7553
7887
|
if (!resolved.match(/\.(ts|tsx|js|jsx)$/)) {
|
|
7554
7888
|
resolved += ".ts";
|
|
7555
7889
|
}
|
|
7556
|
-
resolved =
|
|
7890
|
+
resolved = path15.normalize(resolved).replace(/\\/g, "/");
|
|
7557
7891
|
if (!visited.has(resolved) && current.depth + 1 <= maxDepth) {
|
|
7558
7892
|
queue.push({ file: resolved, depth: current.depth + 1 });
|
|
7559
7893
|
}
|
|
@@ -7570,7 +7904,7 @@ async function validateFindings(options) {
|
|
|
7570
7904
|
if (exclusionSet.isExcluded(normalizedFile, finding.lineRange) || exclusionSet.isExcluded(finding.file, finding.lineRange)) {
|
|
7571
7905
|
continue;
|
|
7572
7906
|
}
|
|
7573
|
-
const absoluteFile =
|
|
7907
|
+
const absoluteFile = path15.isAbsolute(finding.file) ? finding.file : path15.join(projectRoot, finding.file).replace(/\\/g, "/");
|
|
7574
7908
|
if (exclusionSet.isExcluded(absoluteFile, finding.lineRange)) {
|
|
7575
7909
|
continue;
|
|
7576
7910
|
}
|
|
@@ -8091,6 +8425,8 @@ function parseFrontmatter(raw) {
|
|
|
8091
8425
|
const versionStr = map.get("version");
|
|
8092
8426
|
const lastSynced = map.get("last_synced");
|
|
8093
8427
|
const lastManualEdit = map.get("last_manual_edit");
|
|
8428
|
+
const created = map.get("created");
|
|
8429
|
+
const updated = map.get("updated");
|
|
8094
8430
|
if (!project || !versionStr || !lastSynced || !lastManualEdit) {
|
|
8095
8431
|
return Err2(
|
|
8096
8432
|
new Error(
|
|
@@ -8102,7 +8438,10 @@ function parseFrontmatter(raw) {
|
|
|
8102
8438
|
if (isNaN(version)) {
|
|
8103
8439
|
return Err2(new Error("Frontmatter version must be a number"));
|
|
8104
8440
|
}
|
|
8105
|
-
|
|
8441
|
+
const fm = { project, version, lastSynced, lastManualEdit };
|
|
8442
|
+
if (created) fm.created = created;
|
|
8443
|
+
if (updated) fm.updated = updated;
|
|
8444
|
+
return Ok2(fm);
|
|
8106
8445
|
}
|
|
8107
8446
|
function parseMilestones(body) {
|
|
8108
8447
|
const milestones = [];
|
|
@@ -8110,12 +8449,12 @@ function parseMilestones(body) {
|
|
|
8110
8449
|
const h2Matches = [];
|
|
8111
8450
|
let match;
|
|
8112
8451
|
while ((match = h2Pattern.exec(body)) !== null) {
|
|
8113
|
-
h2Matches.push({ heading: match[1], startIndex: match.index });
|
|
8452
|
+
h2Matches.push({ heading: match[1], startIndex: match.index, fullMatch: match[0] });
|
|
8114
8453
|
}
|
|
8115
8454
|
for (let i = 0; i < h2Matches.length; i++) {
|
|
8116
8455
|
const h2 = h2Matches[i];
|
|
8117
8456
|
const nextStart = i + 1 < h2Matches.length ? h2Matches[i + 1].startIndex : body.length;
|
|
8118
|
-
const sectionBody = body.slice(h2.startIndex + h2.
|
|
8457
|
+
const sectionBody = body.slice(h2.startIndex + h2.fullMatch.length, nextStart);
|
|
8119
8458
|
const isBacklog = h2.heading === "Backlog";
|
|
8120
8459
|
const milestoneName = isBacklog ? "Backlog" : h2.heading.replace(/^Milestone:\s*/, "");
|
|
8121
8460
|
const featuresResult = parseFeatures(sectionBody);
|
|
@@ -8130,19 +8469,16 @@ function parseMilestones(body) {
|
|
|
8130
8469
|
}
|
|
8131
8470
|
function parseFeatures(sectionBody) {
|
|
8132
8471
|
const features = [];
|
|
8133
|
-
const h3Pattern = /^### Feature: (.+)$/gm;
|
|
8472
|
+
const h3Pattern = /^### (?:Feature: )?(.+)$/gm;
|
|
8134
8473
|
const h3Matches = [];
|
|
8135
8474
|
let match;
|
|
8136
8475
|
while ((match = h3Pattern.exec(sectionBody)) !== null) {
|
|
8137
|
-
h3Matches.push({ name: match[1], startIndex: match.index });
|
|
8476
|
+
h3Matches.push({ name: match[1], startIndex: match.index, fullMatch: match[0] });
|
|
8138
8477
|
}
|
|
8139
8478
|
for (let i = 0; i < h3Matches.length; i++) {
|
|
8140
8479
|
const h3 = h3Matches[i];
|
|
8141
8480
|
const nextStart = i + 1 < h3Matches.length ? h3Matches[i + 1].startIndex : sectionBody.length;
|
|
8142
|
-
const featureBody = sectionBody.slice(
|
|
8143
|
-
h3.startIndex + `### Feature: ${h3.name}`.length,
|
|
8144
|
-
nextStart
|
|
8145
|
-
);
|
|
8481
|
+
const featureBody = sectionBody.slice(h3.startIndex + h3.fullMatch.length, nextStart);
|
|
8146
8482
|
const featureResult = parseFeatureFields(h3.name, featureBody);
|
|
8147
8483
|
if (!featureResult.ok) return featureResult;
|
|
8148
8484
|
features.push(featureResult.value);
|
|
@@ -8167,10 +8503,10 @@ function parseFeatureFields(name, body) {
|
|
|
8167
8503
|
const status = statusRaw;
|
|
8168
8504
|
const specRaw = fieldMap.get("Spec") ?? EM_DASH;
|
|
8169
8505
|
const spec = specRaw === EM_DASH ? null : specRaw;
|
|
8170
|
-
const plansRaw = fieldMap.get("Plans") ?? EM_DASH;
|
|
8171
|
-
const plans = plansRaw === EM_DASH ? [] : plansRaw.split(",").map((p) => p.trim());
|
|
8172
|
-
const blockedByRaw = fieldMap.get("Blocked by") ?? EM_DASH;
|
|
8173
|
-
const blockedBy = blockedByRaw === EM_DASH ? [] : blockedByRaw.split(",").map((b) => b.trim());
|
|
8506
|
+
const plansRaw = fieldMap.get("Plans") ?? fieldMap.get("Plan") ?? EM_DASH;
|
|
8507
|
+
const plans = plansRaw === EM_DASH || plansRaw === "none" ? [] : plansRaw.split(",").map((p) => p.trim());
|
|
8508
|
+
const blockedByRaw = fieldMap.get("Blocked by") ?? fieldMap.get("Blockers") ?? EM_DASH;
|
|
8509
|
+
const blockedBy = blockedByRaw === EM_DASH || blockedByRaw === "none" ? [] : blockedByRaw.split(",").map((b) => b.trim());
|
|
8174
8510
|
const summary = fieldMap.get("Summary") ?? "";
|
|
8175
8511
|
return Ok2({ name, status, spec, plans, blockedBy, summary });
|
|
8176
8512
|
}
|
|
@@ -8182,11 +8518,17 @@ function serializeRoadmap(roadmap) {
|
|
|
8182
8518
|
lines.push("---");
|
|
8183
8519
|
lines.push(`project: ${roadmap.frontmatter.project}`);
|
|
8184
8520
|
lines.push(`version: ${roadmap.frontmatter.version}`);
|
|
8521
|
+
if (roadmap.frontmatter.created) {
|
|
8522
|
+
lines.push(`created: ${roadmap.frontmatter.created}`);
|
|
8523
|
+
}
|
|
8524
|
+
if (roadmap.frontmatter.updated) {
|
|
8525
|
+
lines.push(`updated: ${roadmap.frontmatter.updated}`);
|
|
8526
|
+
}
|
|
8185
8527
|
lines.push(`last_synced: ${roadmap.frontmatter.lastSynced}`);
|
|
8186
8528
|
lines.push(`last_manual_edit: ${roadmap.frontmatter.lastManualEdit}`);
|
|
8187
8529
|
lines.push("---");
|
|
8188
8530
|
lines.push("");
|
|
8189
|
-
lines.push("#
|
|
8531
|
+
lines.push("# Roadmap");
|
|
8190
8532
|
for (const milestone of roadmap.milestones) {
|
|
8191
8533
|
lines.push("");
|
|
8192
8534
|
lines.push(serializeMilestoneHeading(milestone));
|
|
@@ -8199,25 +8541,26 @@ function serializeRoadmap(roadmap) {
|
|
|
8199
8541
|
return lines.join("\n");
|
|
8200
8542
|
}
|
|
8201
8543
|
function serializeMilestoneHeading(milestone) {
|
|
8202
|
-
return milestone.isBacklog ? "## Backlog" : `##
|
|
8544
|
+
return milestone.isBacklog ? "## Backlog" : `## ${milestone.name}`;
|
|
8203
8545
|
}
|
|
8204
8546
|
function serializeFeature(feature) {
|
|
8205
8547
|
const spec = feature.spec ?? EM_DASH2;
|
|
8206
8548
|
const plans = feature.plans.length > 0 ? feature.plans.join(", ") : EM_DASH2;
|
|
8207
8549
|
const blockedBy = feature.blockedBy.length > 0 ? feature.blockedBy.join(", ") : EM_DASH2;
|
|
8208
8550
|
return [
|
|
8209
|
-
`###
|
|
8551
|
+
`### ${feature.name}`,
|
|
8552
|
+
"",
|
|
8210
8553
|
`- **Status:** ${feature.status}`,
|
|
8211
8554
|
`- **Spec:** ${spec}`,
|
|
8212
|
-
`- **
|
|
8213
|
-
`- **
|
|
8214
|
-
`- **
|
|
8555
|
+
`- **Summary:** ${feature.summary}`,
|
|
8556
|
+
`- **Blockers:** ${blockedBy}`,
|
|
8557
|
+
`- **Plan:** ${plans}`
|
|
8215
8558
|
];
|
|
8216
8559
|
}
|
|
8217
8560
|
|
|
8218
8561
|
// src/roadmap/sync.ts
|
|
8219
|
-
import * as
|
|
8220
|
-
import * as
|
|
8562
|
+
import * as fs16 from "fs";
|
|
8563
|
+
import * as path16 from "path";
|
|
8221
8564
|
import { Ok as Ok3 } from "@harness-engineering/types";
|
|
8222
8565
|
function inferStatus(feature, projectPath, allFeatures) {
|
|
8223
8566
|
if (feature.blockedBy.length > 0) {
|
|
@@ -8232,10 +8575,10 @@ function inferStatus(feature, projectPath, allFeatures) {
|
|
|
8232
8575
|
const featuresWithPlans = allFeatures.filter((f) => f.plans.length > 0);
|
|
8233
8576
|
const useRootState = featuresWithPlans.length <= 1;
|
|
8234
8577
|
if (useRootState) {
|
|
8235
|
-
const rootStatePath =
|
|
8236
|
-
if (
|
|
8578
|
+
const rootStatePath = path16.join(projectPath, ".harness", "state.json");
|
|
8579
|
+
if (fs16.existsSync(rootStatePath)) {
|
|
8237
8580
|
try {
|
|
8238
|
-
const raw =
|
|
8581
|
+
const raw = fs16.readFileSync(rootStatePath, "utf-8");
|
|
8239
8582
|
const state = JSON.parse(raw);
|
|
8240
8583
|
if (state.progress) {
|
|
8241
8584
|
for (const status of Object.values(state.progress)) {
|
|
@@ -8246,16 +8589,16 @@ function inferStatus(feature, projectPath, allFeatures) {
|
|
|
8246
8589
|
}
|
|
8247
8590
|
}
|
|
8248
8591
|
}
|
|
8249
|
-
const sessionsDir =
|
|
8250
|
-
if (
|
|
8592
|
+
const sessionsDir = path16.join(projectPath, ".harness", "sessions");
|
|
8593
|
+
if (fs16.existsSync(sessionsDir)) {
|
|
8251
8594
|
try {
|
|
8252
|
-
const sessionDirs =
|
|
8595
|
+
const sessionDirs = fs16.readdirSync(sessionsDir, { withFileTypes: true });
|
|
8253
8596
|
for (const entry of sessionDirs) {
|
|
8254
8597
|
if (!entry.isDirectory()) continue;
|
|
8255
|
-
const autopilotPath =
|
|
8256
|
-
if (!
|
|
8598
|
+
const autopilotPath = path16.join(sessionsDir, entry.name, "autopilot-state.json");
|
|
8599
|
+
if (!fs16.existsSync(autopilotPath)) continue;
|
|
8257
8600
|
try {
|
|
8258
|
-
const raw =
|
|
8601
|
+
const raw = fs16.readFileSync(autopilotPath, "utf-8");
|
|
8259
8602
|
const autopilot = JSON.parse(raw);
|
|
8260
8603
|
if (!autopilot.phases) continue;
|
|
8261
8604
|
const linkedPhases = autopilot.phases.filter(
|
|
@@ -8335,17 +8678,17 @@ var EmitInteractionInputSchema = z6.object({
|
|
|
8335
8678
|
});
|
|
8336
8679
|
|
|
8337
8680
|
// src/blueprint/scanner.ts
|
|
8338
|
-
import * as
|
|
8339
|
-
import * as
|
|
8681
|
+
import * as fs17 from "fs/promises";
|
|
8682
|
+
import * as path17 from "path";
|
|
8340
8683
|
var ProjectScanner = class {
|
|
8341
8684
|
constructor(rootDir) {
|
|
8342
8685
|
this.rootDir = rootDir;
|
|
8343
8686
|
}
|
|
8344
8687
|
async scan() {
|
|
8345
|
-
let projectName =
|
|
8688
|
+
let projectName = path17.basename(this.rootDir);
|
|
8346
8689
|
try {
|
|
8347
|
-
const pkgPath =
|
|
8348
|
-
const pkgRaw = await
|
|
8690
|
+
const pkgPath = path17.join(this.rootDir, "package.json");
|
|
8691
|
+
const pkgRaw = await fs17.readFile(pkgPath, "utf-8");
|
|
8349
8692
|
const pkg = JSON.parse(pkgRaw);
|
|
8350
8693
|
if (pkg.name) projectName = pkg.name;
|
|
8351
8694
|
} catch {
|
|
@@ -8386,8 +8729,8 @@ var ProjectScanner = class {
|
|
|
8386
8729
|
};
|
|
8387
8730
|
|
|
8388
8731
|
// src/blueprint/generator.ts
|
|
8389
|
-
import * as
|
|
8390
|
-
import * as
|
|
8732
|
+
import * as fs18 from "fs/promises";
|
|
8733
|
+
import * as path18 from "path";
|
|
8391
8734
|
import * as ejs from "ejs";
|
|
8392
8735
|
|
|
8393
8736
|
// src/blueprint/templates.ts
|
|
@@ -8471,19 +8814,19 @@ var BlueprintGenerator = class {
|
|
|
8471
8814
|
styles: STYLES,
|
|
8472
8815
|
scripts: SCRIPTS
|
|
8473
8816
|
});
|
|
8474
|
-
await
|
|
8475
|
-
await
|
|
8817
|
+
await fs18.mkdir(options.outputDir, { recursive: true });
|
|
8818
|
+
await fs18.writeFile(path18.join(options.outputDir, "index.html"), html);
|
|
8476
8819
|
}
|
|
8477
8820
|
};
|
|
8478
8821
|
|
|
8479
8822
|
// src/update-checker.ts
|
|
8480
|
-
import * as
|
|
8481
|
-
import * as
|
|
8823
|
+
import * as fs19 from "fs";
|
|
8824
|
+
import * as path19 from "path";
|
|
8482
8825
|
import * as os from "os";
|
|
8483
8826
|
import { spawn } from "child_process";
|
|
8484
8827
|
function getStatePath() {
|
|
8485
8828
|
const home = process.env["HOME"] || os.homedir();
|
|
8486
|
-
return
|
|
8829
|
+
return path19.join(home, ".harness", "update-check.json");
|
|
8487
8830
|
}
|
|
8488
8831
|
function isUpdateCheckEnabled(configInterval) {
|
|
8489
8832
|
if (process.env["HARNESS_NO_UPDATE_CHECK"] === "1") return false;
|
|
@@ -8496,7 +8839,7 @@ function shouldRunCheck(state, intervalMs) {
|
|
|
8496
8839
|
}
|
|
8497
8840
|
function readCheckState() {
|
|
8498
8841
|
try {
|
|
8499
|
-
const raw =
|
|
8842
|
+
const raw = fs19.readFileSync(getStatePath(), "utf-8");
|
|
8500
8843
|
const parsed = JSON.parse(raw);
|
|
8501
8844
|
if (typeof parsed === "object" && parsed !== null && "lastCheckTime" in parsed && typeof parsed.lastCheckTime === "number" && "currentVersion" in parsed && typeof parsed.currentVersion === "string") {
|
|
8502
8845
|
const state = parsed;
|
|
@@ -8513,7 +8856,7 @@ function readCheckState() {
|
|
|
8513
8856
|
}
|
|
8514
8857
|
function spawnBackgroundCheck(currentVersion) {
|
|
8515
8858
|
const statePath = getStatePath();
|
|
8516
|
-
const stateDir =
|
|
8859
|
+
const stateDir = path19.dirname(statePath);
|
|
8517
8860
|
const script = `
|
|
8518
8861
|
const { execSync } = require('child_process');
|
|
8519
8862
|
const fs = require('fs');
|
|
@@ -8644,6 +8987,7 @@ export {
|
|
|
8644
8987
|
ViolationSchema,
|
|
8645
8988
|
addProvenance,
|
|
8646
8989
|
analyzeDiff,
|
|
8990
|
+
analyzeLearningPatterns,
|
|
8647
8991
|
appendFailure,
|
|
8648
8992
|
appendLearning,
|
|
8649
8993
|
applyFixes,
|
|
@@ -8652,6 +8996,7 @@ export {
|
|
|
8652
8996
|
archModule,
|
|
8653
8997
|
architecture,
|
|
8654
8998
|
archiveFailures,
|
|
8999
|
+
archiveLearnings,
|
|
8655
9000
|
archiveStream,
|
|
8656
9001
|
buildDependencyGraph,
|
|
8657
9002
|
buildExclusionSet,
|
|
@@ -8659,6 +9004,8 @@ export {
|
|
|
8659
9004
|
checkDocCoverage,
|
|
8660
9005
|
checkEligibility,
|
|
8661
9006
|
classifyFinding,
|
|
9007
|
+
clearFailuresCache,
|
|
9008
|
+
clearLearningsCache,
|
|
8662
9009
|
configureFeedback,
|
|
8663
9010
|
constraintRuleId,
|
|
8664
9011
|
contextBudget,
|
|
@@ -8714,16 +9061,20 @@ export {
|
|
|
8714
9061
|
injectionRules,
|
|
8715
9062
|
isSmallSuggestion,
|
|
8716
9063
|
isUpdateCheckEnabled,
|
|
9064
|
+
listActiveSessions,
|
|
8717
9065
|
listStreams,
|
|
9066
|
+
loadBudgetedLearnings,
|
|
8718
9067
|
loadFailures,
|
|
8719
9068
|
loadHandoff,
|
|
8720
9069
|
loadRelevantLearnings,
|
|
9070
|
+
loadSessionSummary,
|
|
8721
9071
|
loadState,
|
|
8722
9072
|
loadStreamIndex,
|
|
8723
9073
|
logAgentAction,
|
|
8724
9074
|
migrateToStreams,
|
|
8725
9075
|
networkRules,
|
|
8726
9076
|
nodeRules,
|
|
9077
|
+
parseDateFromEntry,
|
|
8727
9078
|
parseDiff,
|
|
8728
9079
|
parseManifest,
|
|
8729
9080
|
parseRoadmap,
|
|
@@ -8731,6 +9082,7 @@ export {
|
|
|
8731
9082
|
parseSize,
|
|
8732
9083
|
pathTraversalRules,
|
|
8733
9084
|
previewFix,
|
|
9085
|
+
pruneLearnings,
|
|
8734
9086
|
reactRules,
|
|
8735
9087
|
readCheckState,
|
|
8736
9088
|
readLockfile,
|
|
@@ -8742,6 +9094,7 @@ export {
|
|
|
8742
9094
|
resolveFileToLayer,
|
|
8743
9095
|
resolveModelTier,
|
|
8744
9096
|
resolveRuleSeverity,
|
|
9097
|
+
resolveSessionDir,
|
|
8745
9098
|
resolveStreamPath,
|
|
8746
9099
|
resolveThresholds,
|
|
8747
9100
|
runAll,
|
|
@@ -8768,6 +9121,7 @@ export {
|
|
|
8768
9121
|
syncRoadmap,
|
|
8769
9122
|
touchStream,
|
|
8770
9123
|
trackAction,
|
|
9124
|
+
updateSessionIndex,
|
|
8771
9125
|
validateAgentsMap,
|
|
8772
9126
|
validateBoundaries,
|
|
8773
9127
|
validateCommitMessage,
|
|
@@ -8780,5 +9134,6 @@ export {
|
|
|
8780
9134
|
violationId,
|
|
8781
9135
|
writeConfig,
|
|
8782
9136
|
writeLockfile,
|
|
9137
|
+
writeSessionSummary,
|
|
8783
9138
|
xssRules
|
|
8784
9139
|
};
|