@harness-engineering/cli 2.2.0 → 2.3.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/agents/skills/claude-code/harness-roadmap-pilot/SKILL.md +13 -7
- package/dist/agents/skills/codex/harness-roadmap-pilot/SKILL.md +13 -7
- package/dist/agents/skills/cursor/harness-roadmap-pilot/SKILL.md +13 -7
- package/dist/agents/skills/gemini-cli/harness-roadmap-pilot/SKILL.md +13 -7
- package/dist/{agents-md-L4X4MOWT.js → agents-md-N7YBQPE3.js} +2 -2
- package/dist/{architecture-WJRIYQ45.js → architecture-PP7WERSV.js} +3 -3
- package/dist/{assess-project-PHC2HO3V.js → assess-project-WXFCJQTX.js} +1 -1
- package/dist/bin/harness-mcp.js +13 -13
- package/dist/bin/harness.js +22 -22
- package/dist/{check-phase-gate-646KY67H.js → check-phase-gate-R3KQFBAH.js} +3 -3
- package/dist/{chunk-GOOTYTTV.js → chunk-35FI23SO.js} +855 -428
- package/dist/{chunk-L2IDE7WS.js → chunk-3LPOTTPX.js} +255 -89
- package/dist/{chunk-K4WRQTEF.js → chunk-4DWOMGUM.js} +1661 -434
- package/dist/{chunk-W4AFGFFB.js → chunk-4NFV5UZO.js} +2 -2
- package/dist/{chunk-224XKVLP.js → chunk-5KTTKXQN.js} +1 -1
- package/dist/{chunk-KV7A5OZT.js → chunk-5PPV4WYY.js} +3 -3
- package/dist/{chunk-CPYL6LJR.js → chunk-6CHNY3NJ.js} +1 -1
- package/dist/{chunk-VKDBM3RX.js → chunk-7PGYJO3L.js} +3 -3
- package/dist/{chunk-E7W3YOXL.js → chunk-D2LZUJLP.js} +2 -2
- package/dist/{chunk-4HKVZO7R.js → chunk-DLBMYLYR.js} +1 -1
- package/dist/{chunk-YWD6WXXM.js → chunk-EJPR5VCT.js} +6 -6
- package/dist/{chunk-2ZURBWEV.js → chunk-GTXUUGFG.js} +3 -3
- package/dist/{chunk-OJMO4JBQ.js → chunk-GUIX2TMZ.js} +1 -1
- package/dist/{chunk-TNZYQTEE.js → chunk-KUVPSUG4.js} +1 -1
- package/dist/{chunk-YLTRFZAT.js → chunk-KWD6FF7M.js} +6 -6
- package/dist/{chunk-VJQLUS5I.js → chunk-L3CEN42B.js} +1 -1
- package/dist/{chunk-YLOAQZLA.js → chunk-LM4F5KBK.js} +2 -2
- package/dist/{chunk-JPWICLXQ.js → chunk-MKPRB4PL.js} +1 -1
- package/dist/{chunk-2QPHCR22.js → chunk-MT2MMBUA.js} +5 -5
- package/dist/{chunk-QTUPHTTK.js → chunk-NP2ZI6E3.js} +1 -1
- package/dist/{chunk-LO7ZLV7S.js → chunk-PW3U4YJV.js} +4 -2
- package/dist/{chunk-36X7TRUI.js → chunk-YEBUFRTT.js} +2 -2
- package/dist/{ci-workflow-TOS5X527.js → ci-workflow-GFCXFJ4G.js} +2 -2
- package/dist/{dist-6RT2AVYU.js → dist-I4LWG3IN.js} +23 -1
- package/dist/{docs-HOJMQYDZ.js → docs-UXKDKPJF.js} +3 -3
- package/dist/{engine-D6UYH6DB.js → engine-A63FRDLF.js} +2 -2
- package/dist/{entropy-LZ4IDGZR.js → entropy-IFIP4ZP4.js} +2 -2
- package/dist/{feedback-WV7EQVK7.js → feedback-W36DSJQA.js} +1 -1
- package/dist/{generate-agent-definitions-GFNXJCBY.js → generate-agent-definitions-F4YMKQLP.js} +3 -3
- package/dist/hooks/block-no-verify.js +19 -1
- package/dist/index.d.ts +15 -9
- package/dist/index.js +22 -22
- package/dist/{loader-Z23JTXIZ.js → loader-4UAOGUSS.js} +2 -2
- package/dist/{mcp-JDJGELGD.js → mcp-YW7PLXVQ.js} +13 -13
- package/dist/{performance-7ATLHTQU.js → performance-AZ5UYPOQ.js} +3 -3
- package/dist/{review-pipeline-COBGGXHJ.js → review-pipeline-OLQVGBWJ.js} +2 -2
- package/dist/{runtime-KCBNQUXK.js → runtime-GG3U5HH5.js} +2 -2
- package/dist/{security-MAPRIFQT.js → security-MYTRFSR7.js} +1 -1
- package/dist/{validate-74XTQR7Q.js → validate-QI4V4JY6.js} +3 -3
- package/dist/{validate-cross-check-MI5MZ7MB.js → validate-cross-check-XRSSAXJP.js} +2 -2
- package/package.json +4 -4
|
@@ -19,7 +19,7 @@ import {
|
|
|
19
19
|
__export
|
|
20
20
|
} from "./chunk-KFQGP6VL.js";
|
|
21
21
|
|
|
22
|
-
// ../core/dist/chunk-
|
|
22
|
+
// ../core/dist/chunk-NC4RPKD4.mjs
|
|
23
23
|
import { z } from "zod";
|
|
24
24
|
import { createHash } from "crypto";
|
|
25
25
|
import { minimatch } from "minimatch";
|
|
@@ -69,17 +69,17 @@ function resolveFileToLayer(file, layers) {
|
|
|
69
69
|
}
|
|
70
70
|
return void 0;
|
|
71
71
|
}
|
|
72
|
-
async function fileExists(
|
|
72
|
+
async function fileExists(path46) {
|
|
73
73
|
try {
|
|
74
|
-
await accessAsync(
|
|
74
|
+
await accessAsync(path46, constants.F_OK);
|
|
75
75
|
return true;
|
|
76
76
|
} catch {
|
|
77
77
|
return false;
|
|
78
78
|
}
|
|
79
79
|
}
|
|
80
|
-
async function readFileContent(
|
|
80
|
+
async function readFileContent(path46) {
|
|
81
81
|
try {
|
|
82
|
-
const content = await readFileAsync(
|
|
82
|
+
const content = await readFileAsync(path46, "utf-8");
|
|
83
83
|
return Ok(content);
|
|
84
84
|
} catch (error) {
|
|
85
85
|
return Err(error);
|
|
@@ -485,9 +485,9 @@ async function resolveWasmPath(grammarName) {
|
|
|
485
485
|
const { createRequire } = await import("module");
|
|
486
486
|
const require2 = createRequire(import.meta.url ?? __filename);
|
|
487
487
|
const pkgPath = require2.resolve("tree-sitter-wasms/package.json");
|
|
488
|
-
const
|
|
489
|
-
const pkgDir =
|
|
490
|
-
return
|
|
488
|
+
const path46 = await import("path");
|
|
489
|
+
const pkgDir = path46.dirname(pkgPath);
|
|
490
|
+
return path46.join(pkgDir, "out", `${grammarName}.wasm`);
|
|
491
491
|
}
|
|
492
492
|
async function loadLanguage(lang) {
|
|
493
493
|
const grammarName = GRAMMAR_MAP[lang];
|
|
@@ -1745,11 +1745,16 @@ async function toHaveMaxDepDepth(received, maxDepth) {
|
|
|
1745
1745
|
message: () => pass ? `Expected dependency depth in '${received.scope}' to exceed ${maxDepth} but it was ${maxActual}` : `Module '${received.scope}' has dependency depth ${maxActual} (limit: ${maxDepth})`
|
|
1746
1746
|
};
|
|
1747
1747
|
}
|
|
1748
|
-
var ArchMetricCategorySchema, ViolationSchema, MetricResultSchema, CategoryBaselineSchema, ArchBaselineSchema, CategoryRegressionSchema, ArchDiffResultSchema, ThresholdConfigSchema, ArchConfigSchema, ConstraintRuleSchema, ViolationSnapshotSchema, ViolationHistorySchema, EmergenceConfidenceSchema, EmergentConstraintSuggestionSchema, EmergenceResultSchema, accessAsync, readFileAsync, EXTENSION_BY_LANG, JS_EXT_FALLBACKS, CircularDepsCollector, TypeScriptParser, EXTENSION_MAP, parserCache, initialized, GRAMMAR_MAP, TOP_LEVEL_TYPES, METHOD_TYPES, IDENTIFIER_TYPES, NAME_EXTRACTORS, pythonStrategy, goStrategy, RUST_USE_ARG_TYPES, RUST_PUB_ITEM_TYPES, rustStrategy, JAVA_IMPORT_TYPES, JAVA_EXPORT_TYPES, javaStrategy, STRATEGIES, TreeSitterParser, ParserRegistry, defaultRegistry, LayerViolationCollector, ArchBaselineManager, DEFAULT_THRESHOLDS, FUNCTION_PATTERNS, DECISION_PATTERNS, ComplexityCollector, DEFAULT_THRESHOLDS2, CouplingCollector, ForbiddenImportCollector, ModuleSizeCollector, DepDepthCollector, defaultCollectors, archMatchers;
|
|
1749
|
-
var
|
|
1750
|
-
"../core/dist/chunk-
|
|
1748
|
+
var __defProp, __export2, ArchMetricCategorySchema, ViolationSchema, MetricResultSchema, CategoryBaselineSchema, ArchBaselineSchema, CategoryRegressionSchema, ArchDiffResultSchema, ThresholdConfigSchema, ArchConfigSchema, ConstraintRuleSchema, ViolationSnapshotSchema, ViolationHistorySchema, EmergenceConfidenceSchema, EmergentConstraintSuggestionSchema, EmergenceResultSchema, accessAsync, readFileAsync, EXTENSION_BY_LANG, JS_EXT_FALLBACKS, CircularDepsCollector, TypeScriptParser, EXTENSION_MAP, parserCache, initialized, GRAMMAR_MAP, TOP_LEVEL_TYPES, METHOD_TYPES, IDENTIFIER_TYPES, NAME_EXTRACTORS, pythonStrategy, goStrategy, RUST_USE_ARG_TYPES, RUST_PUB_ITEM_TYPES, rustStrategy, JAVA_IMPORT_TYPES, JAVA_EXPORT_TYPES, javaStrategy, STRATEGIES, TreeSitterParser, ParserRegistry, defaultRegistry, LayerViolationCollector, ArchBaselineManager, DEFAULT_THRESHOLDS, FUNCTION_PATTERNS, DECISION_PATTERNS, ComplexityCollector, DEFAULT_THRESHOLDS2, CouplingCollector, ForbiddenImportCollector, ModuleSizeCollector, DepDepthCollector, defaultCollectors, archMatchers;
|
|
1749
|
+
var init_chunk_NC4RPKD4 = __esm({
|
|
1750
|
+
"../core/dist/chunk-NC4RPKD4.mjs"() {
|
|
1751
1751
|
"use strict";
|
|
1752
1752
|
init_dist();
|
|
1753
|
+
__defProp = Object.defineProperty;
|
|
1754
|
+
__export2 = (target, all) => {
|
|
1755
|
+
for (var name in all)
|
|
1756
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
1757
|
+
};
|
|
1753
1758
|
ArchMetricCategorySchema = z.enum([
|
|
1754
1759
|
"circular-deps",
|
|
1755
1760
|
"layer-violations",
|
|
@@ -1926,11 +1931,11 @@ var init_chunk_CE225WRK = __esm({
|
|
|
1926
1931
|
TypeScriptParser = class {
|
|
1927
1932
|
name = "typescript";
|
|
1928
1933
|
extensions = [".ts", ".tsx", ".mts", ".cts"];
|
|
1929
|
-
async parseFile(
|
|
1930
|
-
const contentResult = await readFileContent(
|
|
1934
|
+
async parseFile(path46) {
|
|
1935
|
+
const contentResult = await readFileContent(path46);
|
|
1931
1936
|
if (!contentResult.ok) {
|
|
1932
1937
|
return Err(
|
|
1933
|
-
createParseError("NOT_FOUND", `File not found: ${
|
|
1938
|
+
createParseError("NOT_FOUND", `File not found: ${path46}`, { path: path46 }, [
|
|
1934
1939
|
"Check that the file exists",
|
|
1935
1940
|
"Verify the path is correct"
|
|
1936
1941
|
])
|
|
@@ -1940,7 +1945,7 @@ var init_chunk_CE225WRK = __esm({
|
|
|
1940
1945
|
const ast = parse(contentResult.value, {
|
|
1941
1946
|
loc: true,
|
|
1942
1947
|
range: true,
|
|
1943
|
-
jsx:
|
|
1948
|
+
jsx: path46.endsWith(".tsx"),
|
|
1944
1949
|
errorOnUnknownASTType: false
|
|
1945
1950
|
});
|
|
1946
1951
|
return Ok({
|
|
@@ -1951,7 +1956,7 @@ var init_chunk_CE225WRK = __esm({
|
|
|
1951
1956
|
} catch (e) {
|
|
1952
1957
|
const error = e;
|
|
1953
1958
|
return Err(
|
|
1954
|
-
createParseError("SYNTAX_ERROR", `Failed to parse ${
|
|
1959
|
+
createParseError("SYNTAX_ERROR", `Failed to parse ${path46}: ${error.message}`, { path: path46 }, [
|
|
1955
1960
|
"Check for syntax errors in the file",
|
|
1956
1961
|
"Ensure valid TypeScript syntax"
|
|
1957
1962
|
])
|
|
@@ -2266,11 +2271,11 @@ var init_chunk_CE225WRK = __esm({
|
|
|
2266
2271
|
this.extensions = extensions;
|
|
2267
2272
|
this.strategy = strategy;
|
|
2268
2273
|
}
|
|
2269
|
-
async parseFile(
|
|
2270
|
-
const contentResult = await readFileContent(
|
|
2274
|
+
async parseFile(path46) {
|
|
2275
|
+
const contentResult = await readFileContent(path46);
|
|
2271
2276
|
if (!contentResult.ok) {
|
|
2272
2277
|
return Err(
|
|
2273
|
-
createParseError("NOT_FOUND", `File not found: ${
|
|
2278
|
+
createParseError("NOT_FOUND", `File not found: ${path46}`, { path: path46 }, [
|
|
2274
2279
|
"Check that the file exists"
|
|
2275
2280
|
])
|
|
2276
2281
|
);
|
|
@@ -2286,7 +2291,7 @@ var init_chunk_CE225WRK = __esm({
|
|
|
2286
2291
|
} catch (e) {
|
|
2287
2292
|
const error = e;
|
|
2288
2293
|
return Err(
|
|
2289
|
-
createParseError("SYNTAX_ERROR", `Failed to parse ${
|
|
2294
|
+
createParseError("SYNTAX_ERROR", `Failed to parse ${path46}: ${error.message}`, { path: path46 }, [
|
|
2290
2295
|
"Check for syntax errors in the file"
|
|
2291
2296
|
])
|
|
2292
2297
|
);
|
|
@@ -2854,6 +2859,7 @@ __export(dist_exports, {
|
|
|
2854
2859
|
CompoundLockHeldError: () => CompoundLockHeldError,
|
|
2855
2860
|
ConfidenceTierSchema: () => ConfidenceTierSchema,
|
|
2856
2861
|
ConfirmationSchema: () => ConfirmationSchema,
|
|
2862
|
+
ConflictError: () => ConflictError,
|
|
2857
2863
|
ConsoleSink: () => ConsoleSink,
|
|
2858
2864
|
ConstraintRuleSchema: () => ConstraintRuleSchema,
|
|
2859
2865
|
ContributingFeatureSchema: () => ContributingFeatureSchema,
|
|
@@ -2873,6 +2879,7 @@ __export(dist_exports, {
|
|
|
2873
2879
|
DepDepthCollector: () => DepDepthCollector,
|
|
2874
2880
|
DirectionSchema: () => DirectionSchema,
|
|
2875
2881
|
EMPTY_SUPPLY_CHAIN: () => EMPTY_SUPPLY_CHAIN,
|
|
2882
|
+
ETagStore: () => ETagStore,
|
|
2876
2883
|
EVIDENCE_SATURATION: () => EVIDENCE_SATURATION,
|
|
2877
2884
|
EXTENSION_MAP: () => EXTENSION_MAP,
|
|
2878
2885
|
EmergenceConfidenceSchema: () => EmergenceConfidenceSchema,
|
|
@@ -3044,6 +3051,7 @@ __export(dist_exports, {
|
|
|
3044
3051
|
createRegionMap: () => createRegionMap,
|
|
3045
3052
|
createSelfReview: () => createSelfReview,
|
|
3046
3053
|
createStream: () => createStream,
|
|
3054
|
+
createTrackerClient: () => createTrackerClient,
|
|
3047
3055
|
crossReferenceUndocumentedFixes: () => crossReferenceUndocumentedFixes,
|
|
3048
3056
|
cryptoRules: () => cryptoRules,
|
|
3049
3057
|
deduplicateCleanupFindings: () => deduplicateCleanupFindings,
|
|
@@ -3104,6 +3112,7 @@ __export(dist_exports, {
|
|
|
3104
3112
|
getParser: () => getParser,
|
|
3105
3113
|
getPhaseCategories: () => getPhaseCategories,
|
|
3106
3114
|
getPulseAdapter: () => getPulseAdapter,
|
|
3115
|
+
getRoadmapMode: () => getRoadmapMode,
|
|
3107
3116
|
getStreamForBranch: () => getStreamForBranch,
|
|
3108
3117
|
getTaintFilePath: () => getTaintFilePath,
|
|
3109
3118
|
getTrustLevel: () => getTrustLevel,
|
|
@@ -3130,17 +3139,21 @@ __export(dist_exports, {
|
|
|
3130
3139
|
loadHandoff: () => loadHandoff,
|
|
3131
3140
|
loadIndexEntries: () => loadIndexEntries,
|
|
3132
3141
|
loadPricingData: () => loadPricingData,
|
|
3142
|
+
loadProjectRoadmapMode: () => loadProjectRoadmapMode,
|
|
3133
3143
|
loadRelevantLearnings: () => loadRelevantLearnings,
|
|
3134
3144
|
loadSessionSummary: () => loadSessionSummary,
|
|
3135
3145
|
loadState: () => loadState,
|
|
3136
3146
|
loadStreamIndex: () => loadStreamIndex,
|
|
3147
|
+
loadTrackerClientConfigFromProject: () => loadTrackerClientConfigFromProject,
|
|
3137
3148
|
loadTrackerSyncConfig: () => loadTrackerSyncConfig,
|
|
3138
3149
|
logAgentAction: () => logAgentAction,
|
|
3150
|
+
makeTrackerConflictBody: () => makeTrackerConflictBody,
|
|
3139
3151
|
mapInjectionFindings: () => mapInjectionFindings,
|
|
3140
3152
|
mapSecurityFindings: () => mapSecurityFindings,
|
|
3141
3153
|
mapSecuritySeverity: () => mapSecuritySeverity,
|
|
3142
3154
|
markProtectedFindings: () => markProtectedFindings,
|
|
3143
3155
|
mcpRules: () => mcpRules,
|
|
3156
|
+
migrate: () => migrate_exports,
|
|
3144
3157
|
migrateToStreams: () => migrateToStreams,
|
|
3145
3158
|
networkRules: () => networkRules,
|
|
3146
3159
|
nodeRules: () => nodeRules,
|
|
@@ -3212,6 +3225,8 @@ __export(dist_exports, {
|
|
|
3212
3225
|
scanForInjection: () => scanForInjection,
|
|
3213
3226
|
scopeContext: () => scopeContext,
|
|
3214
3227
|
scoreRoadmapCandidates: () => scoreRoadmapCandidates,
|
|
3228
|
+
scoreRoadmapCandidatesFileLess: () => scoreRoadmapCandidatesFileLess,
|
|
3229
|
+
scoreRoadmapCandidatesForMode: () => scoreRoadmapCandidatesForMode,
|
|
3215
3230
|
searchSymbols: () => searchSymbols,
|
|
3216
3231
|
secretRules: () => secretRules,
|
|
3217
3232
|
securityFindingId: () => securityFindingId,
|
|
@@ -3249,6 +3264,7 @@ __export(dist_exports, {
|
|
|
3249
3264
|
validateKnowledgeMap: () => validateKnowledgeMap,
|
|
3250
3265
|
validatePatternConfig: () => validatePatternConfig,
|
|
3251
3266
|
validatePulseConfig: () => validatePulseConfig,
|
|
3267
|
+
validateRoadmapMode: () => validateRoadmapMode,
|
|
3252
3268
|
validateSolutionsDir: () => validateSolutionsDir,
|
|
3253
3269
|
violationId: () => violationId,
|
|
3254
3270
|
weeksUntilThreshold: () => weeksUntilThreshold,
|
|
@@ -3294,63 +3310,63 @@ import { existsSync as existsSync9 } from "fs";
|
|
|
3294
3310
|
import { join as join11 } from "path";
|
|
3295
3311
|
import { existsSync as existsSync10 } from "fs";
|
|
3296
3312
|
import { join as join12 } from "path";
|
|
3313
|
+
import * as fs3 from "fs";
|
|
3314
|
+
import * as path3 from "path";
|
|
3297
3315
|
import { minimatch as minimatch2 } from "minimatch";
|
|
3298
3316
|
import { basename as basename2 } from "path";
|
|
3299
|
-
import { join as
|
|
3317
|
+
import { join as join14, basename as basename3 } from "path";
|
|
3300
3318
|
import { basename as basename4, dirname as dirname42 } from "path";
|
|
3301
3319
|
import { z as z3 } from "zod";
|
|
3302
|
-
import * as fs3 from "fs/promises";
|
|
3303
3320
|
import * as fs4 from "fs/promises";
|
|
3304
|
-
import
|
|
3321
|
+
import * as fs5 from "fs/promises";
|
|
3322
|
+
import { join as join15, resolve as resolve42 } from "path";
|
|
3305
3323
|
import { minimatch as minimatch22 } from "minimatch";
|
|
3306
|
-
import { dirname as dirname5, resolve as
|
|
3307
|
-
import { dirname as dirname6, extname as extname2, resolve as
|
|
3324
|
+
import { dirname as dirname5, resolve as resolve5 } from "path";
|
|
3325
|
+
import { dirname as dirname6, extname as extname2, resolve as resolve6 } from "path";
|
|
3308
3326
|
import { minimatch as minimatch3 } from "minimatch";
|
|
3309
3327
|
import { readdirSync, statSync as statSync2 } from "fs";
|
|
3310
|
-
import { join as
|
|
3311
|
-
import * as
|
|
3328
|
+
import { join as join16 } from "path";
|
|
3329
|
+
import * as fs6 from "fs";
|
|
3312
3330
|
import { promisify as promisify2 } from "util";
|
|
3313
|
-
import { dirname as dirname7, basename as basename5, join as
|
|
3331
|
+
import { dirname as dirname7, basename as basename5, join as join17 } from "path";
|
|
3314
3332
|
import { z as z4 } from "zod";
|
|
3315
|
-
import { readFileSync as readFileSync22, writeFileSync as writeFileSync2, mkdirSync as mkdirSync2, existsSync as
|
|
3316
|
-
import { join as
|
|
3333
|
+
import { readFileSync as readFileSync22, writeFileSync as writeFileSync2, mkdirSync as mkdirSync2, existsSync as existsSync12 } from "fs";
|
|
3334
|
+
import { join as join18, dirname as dirname8 } from "path";
|
|
3317
3335
|
import { execFileSync } from "child_process";
|
|
3318
|
-
import * as
|
|
3319
|
-
import * as
|
|
3320
|
-
import { appendFileSync, writeFileSync as writeFileSync22, existsSync as
|
|
3336
|
+
import * as fs7 from "fs";
|
|
3337
|
+
import * as path4 from "path";
|
|
3338
|
+
import { appendFileSync, writeFileSync as writeFileSync22, existsSync as existsSync13, mkdirSync as mkdirSync22 } from "fs";
|
|
3321
3339
|
import { dirname as dirname9 } from "path";
|
|
3322
3340
|
import { z as z5 } from "zod";
|
|
3323
|
-
import { readFileSync as readFileSync4, writeFileSync as writeFileSync3, renameSync as renameSync2, mkdirSync as mkdirSync3, existsSync as
|
|
3341
|
+
import { readFileSync as readFileSync4, writeFileSync as writeFileSync3, renameSync as renameSync2, mkdirSync as mkdirSync3, existsSync as existsSync14 } from "fs";
|
|
3324
3342
|
import { randomBytes as randomBytes2 } from "crypto";
|
|
3325
|
-
import { join as
|
|
3343
|
+
import { join as join20, dirname as dirname10 } from "path";
|
|
3326
3344
|
import { z as z6 } from "zod";
|
|
3327
|
-
import * as fs7 from "fs";
|
|
3328
|
-
import * as path4 from "path";
|
|
3329
3345
|
import * as fs8 from "fs";
|
|
3330
3346
|
import * as path5 from "path";
|
|
3331
3347
|
import * as fs9 from "fs";
|
|
3332
3348
|
import * as path6 from "path";
|
|
3349
|
+
import * as fs10 from "fs";
|
|
3350
|
+
import * as path7 from "path";
|
|
3333
3351
|
import { z as z7 } from "zod";
|
|
3352
|
+
import * as fs14 from "fs";
|
|
3353
|
+
import * as path11 from "path";
|
|
3334
3354
|
import * as fs13 from "fs";
|
|
3335
3355
|
import * as path10 from "path";
|
|
3336
|
-
import * as fs12 from "fs";
|
|
3337
|
-
import * as path9 from "path";
|
|
3338
|
-
import * as fs10 from "fs";
|
|
3339
|
-
import * as path7 from "path";
|
|
3340
|
-
import { execSync } from "child_process";
|
|
3341
|
-
import { z as z8 } from "zod";
|
|
3342
3356
|
import * as fs11 from "fs";
|
|
3343
3357
|
import * as path8 from "path";
|
|
3344
|
-
import
|
|
3345
|
-
import
|
|
3346
|
-
import * as
|
|
3358
|
+
import { execSync } from "child_process";
|
|
3359
|
+
import { z as z8 } from "zod";
|
|
3360
|
+
import * as fs12 from "fs";
|
|
3361
|
+
import * as path9 from "path";
|
|
3347
3362
|
import * as fs15 from "fs";
|
|
3348
3363
|
import * as path12 from "path";
|
|
3364
|
+
import * as crypto2 from "crypto";
|
|
3349
3365
|
import * as fs16 from "fs";
|
|
3350
3366
|
import * as path13 from "path";
|
|
3351
|
-
import * as crypto3 from "crypto";
|
|
3352
3367
|
import * as fs17 from "fs";
|
|
3353
3368
|
import * as path14 from "path";
|
|
3369
|
+
import * as crypto3 from "crypto";
|
|
3354
3370
|
import * as fs18 from "fs";
|
|
3355
3371
|
import * as path15 from "path";
|
|
3356
3372
|
import * as fs19 from "fs";
|
|
@@ -3359,77 +3375,86 @@ import * as fs20 from "fs";
|
|
|
3359
3375
|
import * as path17 from "path";
|
|
3360
3376
|
import * as fs21 from "fs";
|
|
3361
3377
|
import * as path18 from "path";
|
|
3362
|
-
import { execSync as execSync2 } from "child_process";
|
|
3363
3378
|
import * as fs22 from "fs";
|
|
3364
3379
|
import * as path19 from "path";
|
|
3380
|
+
import { execSync as execSync2 } from "child_process";
|
|
3365
3381
|
import * as fs23 from "fs";
|
|
3366
3382
|
import * as path20 from "path";
|
|
3367
3383
|
import * as fs24 from "fs";
|
|
3368
3384
|
import * as path21 from "path";
|
|
3369
3385
|
import * as fs25 from "fs";
|
|
3370
3386
|
import * as path22 from "path";
|
|
3387
|
+
import * as fs26 from "fs";
|
|
3388
|
+
import * as path23 from "path";
|
|
3371
3389
|
import { z as z9 } from "zod";
|
|
3372
|
-
import * as
|
|
3390
|
+
import * as fs28 from "fs/promises";
|
|
3373
3391
|
import { minimatch as minimatch4 } from "minimatch";
|
|
3374
3392
|
import { z as z10 } from "zod";
|
|
3375
|
-
import * as
|
|
3376
|
-
import * as
|
|
3393
|
+
import * as fs27 from "fs";
|
|
3394
|
+
import * as path24 from "path";
|
|
3377
3395
|
import { readFileSync as readFileSync222, writeFileSync as writeFileSync15, unlinkSync as unlinkSync2, mkdirSync as mkdirSync14, readdirSync as readdirSync3 } from "fs";
|
|
3378
|
-
import { join as
|
|
3396
|
+
import { join as join392, dirname as dirname12 } from "path";
|
|
3379
3397
|
import { createHash as createHash32 } from "crypto";
|
|
3380
3398
|
import { z as z11 } from "zod";
|
|
3381
|
-
import { readFileSync as readFileSync23, writeFileSync as writeFileSync16, renameSync as renameSync82, mkdirSync as mkdirSync15, existsSync as
|
|
3399
|
+
import { readFileSync as readFileSync23, writeFileSync as writeFileSync16, renameSync as renameSync82, mkdirSync as mkdirSync15, existsSync as existsSync342 } from "fs";
|
|
3382
3400
|
import { execSync as execSync3 } from "child_process";
|
|
3383
3401
|
import { randomBytes as randomBytes22 } from "crypto";
|
|
3384
|
-
import { isAbsolute as isAbsolute4, join as
|
|
3385
|
-
import * as path24 from "path";
|
|
3402
|
+
import { isAbsolute as isAbsolute4, join as join40, relative as relative32, dirname as dirname13 } from "path";
|
|
3386
3403
|
import * as path25 from "path";
|
|
3387
3404
|
import * as path26 from "path";
|
|
3388
3405
|
import * as path27 from "path";
|
|
3389
|
-
import * as fs28 from "fs";
|
|
3390
3406
|
import * as path28 from "path";
|
|
3391
3407
|
import * as fs29 from "fs";
|
|
3392
3408
|
import * as path29 from "path";
|
|
3393
3409
|
import * as fs30 from "fs";
|
|
3394
|
-
import { z as z12 } from "zod";
|
|
3395
|
-
import * as fs31 from "fs/promises";
|
|
3396
3410
|
import * as path30 from "path";
|
|
3397
|
-
import * as
|
|
3411
|
+
import * as fs31 from "fs";
|
|
3412
|
+
import { parse as parseYaml, stringify as stringifyYaml } from "yaml";
|
|
3413
|
+
import * as fs32 from "fs";
|
|
3398
3414
|
import * as path31 from "path";
|
|
3399
|
-
import * as ejs from "ejs";
|
|
3400
3415
|
import * as fs33 from "fs";
|
|
3401
3416
|
import * as path32 from "path";
|
|
3402
|
-
import
|
|
3403
|
-
import { spawn as spawn2 } from "child_process";
|
|
3404
|
-
import * as fs34 from "fs/promises";
|
|
3417
|
+
import { createHash as createHash4 } from "crypto";
|
|
3405
3418
|
import * as path33 from "path";
|
|
3406
|
-
import
|
|
3419
|
+
import { z as z12 } from "zod";
|
|
3420
|
+
import * as fs34 from "fs/promises";
|
|
3407
3421
|
import * as path34 from "path";
|
|
3408
|
-
import * as
|
|
3422
|
+
import * as fs35 from "fs/promises";
|
|
3409
3423
|
import * as path35 from "path";
|
|
3410
|
-
import * as
|
|
3411
|
-
import * as
|
|
3424
|
+
import * as ejs from "ejs";
|
|
3425
|
+
import * as fs36 from "fs";
|
|
3412
3426
|
import * as path36 from "path";
|
|
3413
|
-
import * as
|
|
3414
|
-
import
|
|
3415
|
-
import
|
|
3416
|
-
import * as fs38 from "fs";
|
|
3427
|
+
import * as os from "os";
|
|
3428
|
+
import { spawn as spawn2 } from "child_process";
|
|
3429
|
+
import * as fs37 from "fs/promises";
|
|
3417
3430
|
import * as path37 from "path";
|
|
3418
|
-
import * as
|
|
3419
|
-
import * as
|
|
3431
|
+
import * as fs38 from "fs";
|
|
3432
|
+
import * as path38 from "path";
|
|
3433
|
+
import * as fs39 from "fs";
|
|
3420
3434
|
import * as path39 from "path";
|
|
3421
|
-
import * as
|
|
3422
|
-
import * as
|
|
3435
|
+
import * as os2 from "os";
|
|
3436
|
+
import * as fs40 from "fs";
|
|
3423
3437
|
import * as path40 from "path";
|
|
3424
|
-
import
|
|
3438
|
+
import * as fs42 from "fs";
|
|
3439
|
+
import * as path42 from "path";
|
|
3440
|
+
import { execFileSync as execFileSync2 } from "child_process";
|
|
3441
|
+
import * as fs41 from "fs";
|
|
3442
|
+
import * as path41 from "path";
|
|
3443
|
+
import * as crypto4 from "crypto";
|
|
3444
|
+
import * as fs43 from "fs";
|
|
3445
|
+
import * as path43 from "path";
|
|
3446
|
+
import * as fs44 from "fs";
|
|
3447
|
+
import * as fs45 from "fs";
|
|
3448
|
+
import * as path44 from "path";
|
|
3449
|
+
import { readFileSync as readFileSync38 } from "fs";
|
|
3425
3450
|
import { fileURLToPath } from "url";
|
|
3426
|
-
import { dirname as dirname18, join as
|
|
3451
|
+
import { dirname as dirname18, join as join612 } from "path";
|
|
3427
3452
|
import { execFile } from "child_process";
|
|
3428
3453
|
import { promisify as promisify22 } from "util";
|
|
3429
3454
|
import { execFile as execFile2 } from "child_process";
|
|
3430
3455
|
import { promisify as promisify3 } from "util";
|
|
3431
|
-
import * as
|
|
3432
|
-
import * as
|
|
3456
|
+
import * as fs46 from "fs/promises";
|
|
3457
|
+
import * as path45 from "path";
|
|
3433
3458
|
async function validateFileStructure(projectPath, conventions) {
|
|
3434
3459
|
const missing = [];
|
|
3435
3460
|
const unexpected = [];
|
|
@@ -3465,15 +3490,15 @@ function validateConfig(data, schema) {
|
|
|
3465
3490
|
let message = "Configuration validation failed";
|
|
3466
3491
|
const suggestions = [];
|
|
3467
3492
|
if (firstError) {
|
|
3468
|
-
const
|
|
3469
|
-
const pathDisplay =
|
|
3493
|
+
const path46 = firstError.path.join(".");
|
|
3494
|
+
const pathDisplay = path46 ? ` at "${path46}"` : "";
|
|
3470
3495
|
if (firstError.code === "invalid_type") {
|
|
3471
3496
|
const received = firstError.received;
|
|
3472
3497
|
const expected = firstError.expected;
|
|
3473
3498
|
if (received === "undefined") {
|
|
3474
3499
|
code = "MISSING_FIELD";
|
|
3475
3500
|
message = `Missing required field${pathDisplay}: ${firstError.message}`;
|
|
3476
|
-
suggestions.push(`Field "${
|
|
3501
|
+
suggestions.push(`Field "${path46}" is required and must be of type "${expected}"`);
|
|
3477
3502
|
} else {
|
|
3478
3503
|
code = "INVALID_TYPE";
|
|
3479
3504
|
message = `Invalid type${pathDisplay}: ${firstError.message}`;
|
|
@@ -3689,7 +3714,7 @@ function isAgnixDisabled() {
|
|
|
3689
3714
|
return v === "1" || v === "true";
|
|
3690
3715
|
}
|
|
3691
3716
|
function runAgnix(cwd, strict, binPath, timeoutMs = DEFAULT_AGNIX_TIMEOUT_MS, spawnFn = spawn) {
|
|
3692
|
-
return new Promise((
|
|
3717
|
+
return new Promise((resolve9) => {
|
|
3693
3718
|
const args = ["--format", "json"];
|
|
3694
3719
|
if (strict) args.push("--strict");
|
|
3695
3720
|
args.push(cwd);
|
|
@@ -3703,7 +3728,7 @@ function runAgnix(cwd, strict, binPath, timeoutMs = DEFAULT_AGNIX_TIMEOUT_MS, sp
|
|
|
3703
3728
|
if (settled) return;
|
|
3704
3729
|
settled = true;
|
|
3705
3730
|
clearTimeout(timer);
|
|
3706
|
-
|
|
3731
|
+
resolve9(outcome);
|
|
3707
3732
|
};
|
|
3708
3733
|
const timer = setTimeout(() => {
|
|
3709
3734
|
child.kill("SIGKILL");
|
|
@@ -3807,16 +3832,16 @@ function makeFinding(input) {
|
|
|
3807
3832
|
if (input.suggestion) finding.suggestion = input.suggestion;
|
|
3808
3833
|
return finding;
|
|
3809
3834
|
}
|
|
3810
|
-
function readTextSafe(
|
|
3835
|
+
function readTextSafe(path46) {
|
|
3811
3836
|
try {
|
|
3812
|
-
return readFileSync2(
|
|
3837
|
+
return readFileSync2(path46, "utf-8");
|
|
3813
3838
|
} catch {
|
|
3814
3839
|
return null;
|
|
3815
3840
|
}
|
|
3816
3841
|
}
|
|
3817
|
-
function safeFileSize(
|
|
3842
|
+
function safeFileSize(path46) {
|
|
3818
3843
|
try {
|
|
3819
|
-
return statSync(
|
|
3844
|
+
return statSync(path46).size;
|
|
3820
3845
|
} catch {
|
|
3821
3846
|
return null;
|
|
3822
3847
|
}
|
|
@@ -4361,27 +4386,27 @@ function extractSections(content) {
|
|
|
4361
4386
|
}
|
|
4362
4387
|
return sections.map((section) => buildAgentMapSection(section, lines));
|
|
4363
4388
|
}
|
|
4364
|
-
function isExternalLink(
|
|
4365
|
-
return
|
|
4389
|
+
function isExternalLink(path46) {
|
|
4390
|
+
return path46.startsWith("http://") || path46.startsWith("https://") || path46.startsWith("#") || path46.startsWith("mailto:");
|
|
4366
4391
|
}
|
|
4367
4392
|
function resolveLinkPath(linkPath, baseDir) {
|
|
4368
4393
|
return linkPath.startsWith(".") ? join8(baseDir, linkPath) : linkPath;
|
|
4369
4394
|
}
|
|
4370
|
-
async function validateAgentsMap(
|
|
4371
|
-
const contentResult = await readFileContent(
|
|
4395
|
+
async function validateAgentsMap(path46 = "./AGENTS.md") {
|
|
4396
|
+
const contentResult = await readFileContent(path46);
|
|
4372
4397
|
if (!contentResult.ok) {
|
|
4373
4398
|
return Err(
|
|
4374
4399
|
createError(
|
|
4375
4400
|
"PARSE_ERROR",
|
|
4376
4401
|
`Failed to read AGENTS.md: ${contentResult.error.message}`,
|
|
4377
|
-
{ path:
|
|
4402
|
+
{ path: path46 },
|
|
4378
4403
|
["Ensure the file exists", "Check file permissions"]
|
|
4379
4404
|
)
|
|
4380
4405
|
);
|
|
4381
4406
|
}
|
|
4382
4407
|
const content = contentResult.value;
|
|
4383
4408
|
const sections = extractSections(content);
|
|
4384
|
-
const baseDir = dirname22(
|
|
4409
|
+
const baseDir = dirname22(path46);
|
|
4385
4410
|
const sectionTitles = sections.map((s) => s.title);
|
|
4386
4411
|
const missingSections = REQUIRED_SECTIONS.filter(
|
|
4387
4412
|
(required) => !sectionTitles.some((title) => title.toLowerCase().includes(required.toLowerCase()))
|
|
@@ -4725,6 +4750,45 @@ function promoteWarningsToErrors(findings) {
|
|
|
4725
4750
|
function hasErrors(findings) {
|
|
4726
4751
|
return findings.some((f) => f.severity === "error");
|
|
4727
4752
|
}
|
|
4753
|
+
function getRoadmapMode(config) {
|
|
4754
|
+
if (!config || !config.roadmap) return "file-backed";
|
|
4755
|
+
const mode = config.roadmap.mode;
|
|
4756
|
+
return mode === "file-less" ? "file-less" : "file-backed";
|
|
4757
|
+
}
|
|
4758
|
+
function validateRoadmapMode(config, projectRoot) {
|
|
4759
|
+
const root = path3.resolve(projectRoot);
|
|
4760
|
+
const mode = getRoadmapMode(config);
|
|
4761
|
+
if (mode === "file-backed") return Ok(void 0);
|
|
4762
|
+
const tracker = config.roadmap?.tracker;
|
|
4763
|
+
if (!tracker || typeof tracker !== "object") {
|
|
4764
|
+
return Err(
|
|
4765
|
+
createError(
|
|
4766
|
+
"ROADMAP_MODE_MISSING_TRACKER",
|
|
4767
|
+
'roadmap.mode is "file-less" but roadmap.tracker is not configured. File-less mode requires an external tracker as the source of truth.',
|
|
4768
|
+
{ mode },
|
|
4769
|
+
[
|
|
4770
|
+
'Configure roadmap.tracker in harness.config.json (e.g., kind: "github", repo: "owner/name").',
|
|
4771
|
+
'Or set roadmap.mode to "file-backed" if you want to keep using docs/roadmap.md.'
|
|
4772
|
+
]
|
|
4773
|
+
)
|
|
4774
|
+
);
|
|
4775
|
+
}
|
|
4776
|
+
const roadmapPath = path3.join(root, "docs", "roadmap.md");
|
|
4777
|
+
if (fs3.existsSync(roadmapPath)) {
|
|
4778
|
+
return Err(
|
|
4779
|
+
createError(
|
|
4780
|
+
"ROADMAP_MODE_FILE_PRESENT",
|
|
4781
|
+
'roadmap.mode is "file-less" but docs/roadmap.md still exists. In file-less mode the tracker is canonical; the markdown file must be migrated. Run `harness roadmap migrate --to=file-less` to migrate features into the tracker.',
|
|
4782
|
+
{ mode, roadmapPath },
|
|
4783
|
+
[
|
|
4784
|
+
"Run `harness roadmap migrate --to=file-less` to migrate features into the tracker.",
|
|
4785
|
+
'Or set roadmap.mode to "file-backed" if you want to keep docs/roadmap.md.'
|
|
4786
|
+
]
|
|
4787
|
+
)
|
|
4788
|
+
);
|
|
4789
|
+
}
|
|
4790
|
+
return Ok(void 0);
|
|
4791
|
+
}
|
|
4728
4792
|
function determineImportance(filePath) {
|
|
4729
4793
|
const name = basename2(filePath).toLowerCase();
|
|
4730
4794
|
if (name === "index.ts" || name === "index.js" || name === "main.ts") {
|
|
@@ -4821,8 +4885,8 @@ async function checkDocCoverage(domain, options = {}) {
|
|
|
4821
4885
|
);
|
|
4822
4886
|
}
|
|
4823
4887
|
}
|
|
4824
|
-
function suggestFix(
|
|
4825
|
-
const targetName = basename3(
|
|
4888
|
+
function suggestFix(path46, existingFiles) {
|
|
4889
|
+
const targetName = basename3(path46).toLowerCase();
|
|
4826
4890
|
const similar = existingFiles.find((file) => {
|
|
4827
4891
|
const fileName = basename3(file).toLowerCase();
|
|
4828
4892
|
return fileName.includes(targetName) || targetName.includes(fileName);
|
|
@@ -4830,10 +4894,10 @@ function suggestFix(path42, existingFiles) {
|
|
|
4830
4894
|
if (similar) {
|
|
4831
4895
|
return `Did you mean "${similar}"?`;
|
|
4832
4896
|
}
|
|
4833
|
-
return `Create the file "${
|
|
4897
|
+
return `Create the file "${path46}" or remove the link`;
|
|
4834
4898
|
}
|
|
4835
4899
|
async function validateKnowledgeMap(rootDir = process.cwd()) {
|
|
4836
|
-
const agentsPath =
|
|
4900
|
+
const agentsPath = join14(rootDir, "AGENTS.md");
|
|
4837
4901
|
const agentsResult = await validateAgentsMap(agentsPath);
|
|
4838
4902
|
if (!agentsResult.ok) {
|
|
4839
4903
|
return Err(agentsResult.error);
|
|
@@ -5188,8 +5252,8 @@ function createBoundaryValidator(schema, name) {
|
|
|
5188
5252
|
return Ok(result.data);
|
|
5189
5253
|
}
|
|
5190
5254
|
const suggestions = result.error.issues.map((issue) => {
|
|
5191
|
-
const
|
|
5192
|
-
return
|
|
5255
|
+
const path46 = issue.path.join(".");
|
|
5256
|
+
return path46 ? `${path46}: ${issue.message}` : issue.message;
|
|
5193
5257
|
});
|
|
5194
5258
|
return Err(
|
|
5195
5259
|
createError(
|
|
@@ -5235,7 +5299,7 @@ function validateBoundaries(boundaries, data) {
|
|
|
5235
5299
|
async function writeConfig(filePath, content) {
|
|
5236
5300
|
try {
|
|
5237
5301
|
const json = JSON.stringify(content, null, 2) + "\n";
|
|
5238
|
-
await
|
|
5302
|
+
await fs4.writeFile(filePath, json, "utf-8");
|
|
5239
5303
|
return Ok(void 0);
|
|
5240
5304
|
} catch (error) {
|
|
5241
5305
|
return Err(error instanceof Error ? error : new Error(String(error)));
|
|
@@ -5509,7 +5573,7 @@ function deepMergeConstraints(localConfig, bundleConstraints, _existingContribut
|
|
|
5509
5573
|
async function readLockfile(lockfilePath) {
|
|
5510
5574
|
let raw;
|
|
5511
5575
|
try {
|
|
5512
|
-
raw = await
|
|
5576
|
+
raw = await fs5.readFile(lockfilePath, "utf-8");
|
|
5513
5577
|
} catch (err) {
|
|
5514
5578
|
if (isNodeError(err) && err.code === "ENOENT") {
|
|
5515
5579
|
return { ok: true, value: null };
|
|
@@ -5791,9 +5855,9 @@ function parseProtectedRegions(files) {
|
|
|
5791
5855
|
};
|
|
5792
5856
|
}
|
|
5793
5857
|
function collectFieldEntries(rootDir, field) {
|
|
5794
|
-
if (typeof field === "string") return [
|
|
5858
|
+
if (typeof field === "string") return [resolve42(rootDir, field)];
|
|
5795
5859
|
if (typeof field === "object" && field !== null) {
|
|
5796
|
-
return Object.values(field).filter((v) => typeof v === "string").map((v) =>
|
|
5860
|
+
return Object.values(field).filter((v) => typeof v === "string").map((v) => resolve42(rootDir, v));
|
|
5797
5861
|
}
|
|
5798
5862
|
return [];
|
|
5799
5863
|
}
|
|
@@ -5801,7 +5865,7 @@ function extractPackageEntries(rootDir, pkg) {
|
|
|
5801
5865
|
const entries = [];
|
|
5802
5866
|
entries.push(...collectFieldEntries(rootDir, pkg["exports"]));
|
|
5803
5867
|
if (entries.length === 0 && typeof pkg["main"] === "string") {
|
|
5804
|
-
entries.push(
|
|
5868
|
+
entries.push(resolve42(rootDir, pkg["main"]));
|
|
5805
5869
|
}
|
|
5806
5870
|
if (pkg["bin"]) {
|
|
5807
5871
|
entries.push(...collectFieldEntries(rootDir, pkg["bin"]));
|
|
@@ -5810,9 +5874,9 @@ function extractPackageEntries(rootDir, pkg) {
|
|
|
5810
5874
|
}
|
|
5811
5875
|
async function resolveEntryPoints(rootDir, explicitEntries) {
|
|
5812
5876
|
if (explicitEntries && explicitEntries.length > 0) {
|
|
5813
|
-
return Ok(explicitEntries.map((e) =>
|
|
5877
|
+
return Ok(explicitEntries.map((e) => resolve42(rootDir, e)));
|
|
5814
5878
|
}
|
|
5815
|
-
const pkgPath =
|
|
5879
|
+
const pkgPath = join15(rootDir, "package.json");
|
|
5816
5880
|
if (await fileExists(pkgPath)) {
|
|
5817
5881
|
const pkgContent = await readFileContent(pkgPath);
|
|
5818
5882
|
if (pkgContent.ok) {
|
|
@@ -5826,7 +5890,7 @@ async function resolveEntryPoints(rootDir, explicitEntries) {
|
|
|
5826
5890
|
}
|
|
5827
5891
|
const conventions = ["src/index.ts", "src/main.ts", "index.ts", "main.ts"];
|
|
5828
5892
|
for (const conv of conventions) {
|
|
5829
|
-
const convPath =
|
|
5893
|
+
const convPath = join15(rootDir, conv);
|
|
5830
5894
|
if (await fileExists(convPath)) {
|
|
5831
5895
|
return Ok([convPath]);
|
|
5832
5896
|
}
|
|
@@ -5893,22 +5957,22 @@ function extractInlineRefs(content) {
|
|
|
5893
5957
|
}
|
|
5894
5958
|
return refs;
|
|
5895
5959
|
}
|
|
5896
|
-
async function parseDocumentationFile(
|
|
5897
|
-
const contentResult = await readFileContent(
|
|
5960
|
+
async function parseDocumentationFile(path46) {
|
|
5961
|
+
const contentResult = await readFileContent(path46);
|
|
5898
5962
|
if (!contentResult.ok) {
|
|
5899
5963
|
return Err(
|
|
5900
5964
|
createEntropyError(
|
|
5901
5965
|
"PARSE_ERROR",
|
|
5902
|
-
`Failed to read documentation file: ${
|
|
5903
|
-
{ file:
|
|
5966
|
+
`Failed to read documentation file: ${path46}`,
|
|
5967
|
+
{ file: path46 },
|
|
5904
5968
|
["Check that the file exists"]
|
|
5905
5969
|
)
|
|
5906
5970
|
);
|
|
5907
5971
|
}
|
|
5908
5972
|
const content = contentResult.value;
|
|
5909
|
-
const type =
|
|
5973
|
+
const type = path46.endsWith(".md") ? "markdown" : "text";
|
|
5910
5974
|
return Ok({
|
|
5911
|
-
path:
|
|
5975
|
+
path: path46,
|
|
5912
5976
|
type,
|
|
5913
5977
|
content,
|
|
5914
5978
|
codeBlocks: extractCodeBlocks(content),
|
|
@@ -6005,7 +6069,7 @@ function extractAllCodeReferences(docs) {
|
|
|
6005
6069
|
async function buildSnapshot2(config) {
|
|
6006
6070
|
const startTime = Date.now();
|
|
6007
6071
|
const parser = config.parser || new TypeScriptParser();
|
|
6008
|
-
const rootDir =
|
|
6072
|
+
const rootDir = resolve42(config.rootDir);
|
|
6009
6073
|
const entryPointsResult = await resolveEntryPoints(rootDir, config.entryPoints);
|
|
6010
6074
|
if (!entryPointsResult.ok) {
|
|
6011
6075
|
return Err(entryPointsResult.error);
|
|
@@ -6178,7 +6242,7 @@ async function checkStructureDrift(snapshot, _config) {
|
|
|
6178
6242
|
for (const doc of snapshot.docs) {
|
|
6179
6243
|
const fileLinks = extractFileLinks(doc.content);
|
|
6180
6244
|
for (const { link, line } of fileLinks) {
|
|
6181
|
-
const resolvedPath =
|
|
6245
|
+
const resolvedPath = resolve5(dirname5(doc.path), link);
|
|
6182
6246
|
const exists = await fileExists(resolvedPath);
|
|
6183
6247
|
if (!exists) {
|
|
6184
6248
|
drifts.push({
|
|
@@ -6280,7 +6344,7 @@ function resolveImportToFile(importSource, fromFile, snapshot, fileIndex) {
|
|
|
6280
6344
|
}
|
|
6281
6345
|
const hasFile = fileIndex ? (p) => fileIndex.has(p) : (p) => snapshot.files.some((f) => f.path === p);
|
|
6282
6346
|
const fromDir = dirname6(fromFile);
|
|
6283
|
-
const resolved =
|
|
6347
|
+
const resolved = resolve6(fromDir, importSource);
|
|
6284
6348
|
const sourceExt = extname2(resolved);
|
|
6285
6349
|
const fallbacks = JS_EXT_FALLBACKS2[sourceExt];
|
|
6286
6350
|
if (fallbacks) {
|
|
@@ -6290,7 +6354,7 @@ function resolveImportToFile(importSource, fromFile, snapshot, fileIndex) {
|
|
|
6290
6354
|
if (hasFile(candidate)) return candidate;
|
|
6291
6355
|
}
|
|
6292
6356
|
for (const indexExt of [".ts", ".tsx", ".jsx"]) {
|
|
6293
|
-
const indexPath2 =
|
|
6357
|
+
const indexPath2 = resolve6(base, "index" + indexExt);
|
|
6294
6358
|
if (hasFile(indexPath2)) return indexPath2;
|
|
6295
6359
|
}
|
|
6296
6360
|
}
|
|
@@ -6301,7 +6365,7 @@ function resolveImportToFile(importSource, fromFile, snapshot, fileIndex) {
|
|
|
6301
6365
|
if (hasFile(candidate)) return candidate;
|
|
6302
6366
|
}
|
|
6303
6367
|
for (const indexExt of [".ts", ".tsx"]) {
|
|
6304
|
-
const indexPath2 =
|
|
6368
|
+
const indexPath2 = resolve6(resolved, "index" + indexExt);
|
|
6305
6369
|
if (hasFile(indexPath2)) return indexPath2;
|
|
6306
6370
|
}
|
|
6307
6371
|
}
|
|
@@ -6818,7 +6882,7 @@ function dirSize(dirPath) {
|
|
|
6818
6882
|
}
|
|
6819
6883
|
for (const entry of entries) {
|
|
6820
6884
|
if (entry === "node_modules" || entry === ".git") continue;
|
|
6821
|
-
const fullPath =
|
|
6885
|
+
const fullPath = join16(dirPath, entry);
|
|
6822
6886
|
try {
|
|
6823
6887
|
const stat2 = statSync2(fullPath);
|
|
6824
6888
|
if (stat2.isDirectory()) {
|
|
@@ -6838,7 +6902,7 @@ async function detectSizeBudgetViolations(rootDir, config) {
|
|
|
6838
6902
|
let packagesChecked = 0;
|
|
6839
6903
|
for (const [pkgPath, budget] of Object.entries(budgets)) {
|
|
6840
6904
|
packagesChecked++;
|
|
6841
|
-
const distPath =
|
|
6905
|
+
const distPath = join16(rootDir, pkgPath, "dist");
|
|
6842
6906
|
const currentSize = dirSize(distPath);
|
|
6843
6907
|
if (budget.warn) {
|
|
6844
6908
|
const budgetBytes = parseSize(budget.warn);
|
|
@@ -7082,7 +7146,7 @@ function previewFix(fix) {
|
|
|
7082
7146
|
}
|
|
7083
7147
|
}
|
|
7084
7148
|
async function createBackup(filePath, backupDir) {
|
|
7085
|
-
const backupPath =
|
|
7149
|
+
const backupPath = join17(backupDir, `${Date.now()}-${basename5(filePath)}`);
|
|
7086
7150
|
try {
|
|
7087
7151
|
await mkdir22(dirname7(backupPath), { recursive: true });
|
|
7088
7152
|
await copyFile2(filePath, backupPath);
|
|
@@ -7457,12 +7521,12 @@ function parseDiffHeader(part) {
|
|
|
7457
7521
|
return headerMatch[2];
|
|
7458
7522
|
}
|
|
7459
7523
|
function parseDiffPart(part) {
|
|
7460
|
-
const
|
|
7461
|
-
if (!
|
|
7524
|
+
const path46 = parseDiffHeader(part);
|
|
7525
|
+
if (!path46) return null;
|
|
7462
7526
|
const additionRegex = /^\+(?!\+\+)/gm;
|
|
7463
7527
|
const deletionRegex = /^-(?!--)/gm;
|
|
7464
7528
|
return {
|
|
7465
|
-
path:
|
|
7529
|
+
path: path46,
|
|
7466
7530
|
status: detectFileStatus(part),
|
|
7467
7531
|
additions: (part.match(additionRegex) || []).length,
|
|
7468
7532
|
deletions: (part.match(deletionRegex) || []).length
|
|
@@ -8247,10 +8311,10 @@ function detectEmergentConstraints(history, options) {
|
|
|
8247
8311
|
return { suggestions, totalViolationsAnalyzed, windowWeeks, minOccurrences };
|
|
8248
8312
|
}
|
|
8249
8313
|
function streamsDir(projectPath) {
|
|
8250
|
-
return
|
|
8314
|
+
return path8.join(projectPath, HARNESS_DIR, STREAMS_DIR);
|
|
8251
8315
|
}
|
|
8252
8316
|
function indexPath(projectPath) {
|
|
8253
|
-
return
|
|
8317
|
+
return path8.join(streamsDir(projectPath), INDEX_FILE);
|
|
8254
8318
|
}
|
|
8255
8319
|
function validateStreamName(name) {
|
|
8256
8320
|
if (!STREAM_NAME_REGEX.test(name)) {
|
|
@@ -8264,11 +8328,11 @@ function validateStreamName(name) {
|
|
|
8264
8328
|
}
|
|
8265
8329
|
async function loadStreamIndex(projectPath) {
|
|
8266
8330
|
const idxPath = indexPath(projectPath);
|
|
8267
|
-
if (!
|
|
8331
|
+
if (!fs11.existsSync(idxPath)) {
|
|
8268
8332
|
return Ok({ ...DEFAULT_STREAM_INDEX, streams: {} });
|
|
8269
8333
|
}
|
|
8270
8334
|
try {
|
|
8271
|
-
const raw =
|
|
8335
|
+
const raw = fs11.readFileSync(idxPath, "utf-8");
|
|
8272
8336
|
const parsed = JSON.parse(raw);
|
|
8273
8337
|
const result = StreamIndexSchema.safeParse(parsed);
|
|
8274
8338
|
if (!result.success) {
|
|
@@ -8286,8 +8350,8 @@ async function loadStreamIndex(projectPath) {
|
|
|
8286
8350
|
async function saveStreamIndex(projectPath, index) {
|
|
8287
8351
|
const dir = streamsDir(projectPath);
|
|
8288
8352
|
try {
|
|
8289
|
-
|
|
8290
|
-
|
|
8353
|
+
fs11.mkdirSync(dir, { recursive: true });
|
|
8354
|
+
fs11.writeFileSync(indexPath(projectPath), JSON.stringify(index, null, 2));
|
|
8291
8355
|
return Ok(void 0);
|
|
8292
8356
|
} catch (error) {
|
|
8293
8357
|
return Err(
|
|
@@ -8326,18 +8390,18 @@ async function resolveStreamPath(projectPath, options) {
|
|
|
8326
8390
|
)
|
|
8327
8391
|
);
|
|
8328
8392
|
}
|
|
8329
|
-
return Ok(
|
|
8393
|
+
return Ok(path8.join(streamsDir(projectPath), options.stream));
|
|
8330
8394
|
}
|
|
8331
8395
|
const branch = getCurrentBranch(projectPath);
|
|
8332
8396
|
if (branch && branch !== "main" && branch !== "master") {
|
|
8333
8397
|
for (const [name, info] of Object.entries(index.streams)) {
|
|
8334
8398
|
if (info.branch === branch) {
|
|
8335
|
-
return Ok(
|
|
8399
|
+
return Ok(path8.join(streamsDir(projectPath), name));
|
|
8336
8400
|
}
|
|
8337
8401
|
}
|
|
8338
8402
|
}
|
|
8339
8403
|
if (index.activeStream && index.streams[index.activeStream]) {
|
|
8340
|
-
return Ok(
|
|
8404
|
+
return Ok(path8.join(streamsDir(projectPath), index.activeStream));
|
|
8341
8405
|
}
|
|
8342
8406
|
return Err(
|
|
8343
8407
|
new Error(
|
|
@@ -8365,9 +8429,9 @@ async function createStream(projectPath, name, branch) {
|
|
|
8365
8429
|
if (index.streams[name]) {
|
|
8366
8430
|
return Err(new Error(`Stream '${name}' already exists`));
|
|
8367
8431
|
}
|
|
8368
|
-
const streamPath =
|
|
8432
|
+
const streamPath = path8.join(streamsDir(projectPath), name);
|
|
8369
8433
|
try {
|
|
8370
|
-
|
|
8434
|
+
fs11.mkdirSync(streamPath, { recursive: true });
|
|
8371
8435
|
} catch (error) {
|
|
8372
8436
|
return Err(
|
|
8373
8437
|
new Error(
|
|
@@ -8408,12 +8472,12 @@ async function archiveStream(projectPath, name) {
|
|
|
8408
8472
|
if (!index.streams[name]) {
|
|
8409
8473
|
return Err(new Error(`Stream '${name}' not found`));
|
|
8410
8474
|
}
|
|
8411
|
-
const streamPath =
|
|
8412
|
-
const archiveDir =
|
|
8475
|
+
const streamPath = path8.join(streamsDir(projectPath), name);
|
|
8476
|
+
const archiveDir = path8.join(projectPath, HARNESS_DIR, "archive", "streams");
|
|
8413
8477
|
try {
|
|
8414
|
-
|
|
8478
|
+
fs11.mkdirSync(archiveDir, { recursive: true });
|
|
8415
8479
|
const date = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
|
|
8416
|
-
|
|
8480
|
+
fs11.renameSync(streamPath, path8.join(archiveDir, `${name}-${date}`));
|
|
8417
8481
|
} catch (error) {
|
|
8418
8482
|
return Err(
|
|
8419
8483
|
new Error(
|
|
@@ -8434,19 +8498,19 @@ function getStreamForBranch(index, branch) {
|
|
|
8434
8498
|
return null;
|
|
8435
8499
|
}
|
|
8436
8500
|
async function migrateToStreams(projectPath) {
|
|
8437
|
-
const harnessDir =
|
|
8438
|
-
if (
|
|
8501
|
+
const harnessDir = path8.join(projectPath, HARNESS_DIR);
|
|
8502
|
+
if (fs11.existsSync(indexPath(projectPath))) {
|
|
8439
8503
|
return Ok(void 0);
|
|
8440
8504
|
}
|
|
8441
|
-
const filesToMove = STATE_FILES.filter((f) =>
|
|
8505
|
+
const filesToMove = STATE_FILES.filter((f) => fs11.existsSync(path8.join(harnessDir, f)));
|
|
8442
8506
|
if (filesToMove.length === 0) {
|
|
8443
8507
|
return Ok(void 0);
|
|
8444
8508
|
}
|
|
8445
|
-
const defaultDir =
|
|
8509
|
+
const defaultDir = path8.join(streamsDir(projectPath), "default");
|
|
8446
8510
|
try {
|
|
8447
|
-
|
|
8511
|
+
fs11.mkdirSync(defaultDir, { recursive: true });
|
|
8448
8512
|
for (const file of filesToMove) {
|
|
8449
|
-
|
|
8513
|
+
fs11.renameSync(path8.join(harnessDir, file), path8.join(defaultDir, file));
|
|
8450
8514
|
}
|
|
8451
8515
|
} catch (error) {
|
|
8452
8516
|
return Err(
|
|
@@ -8476,26 +8540,26 @@ function resolveSessionDir(projectPath, sessionSlug, options) {
|
|
|
8476
8540
|
new Error(`Invalid session slug '${sessionSlug}': must not contain path traversal characters`)
|
|
8477
8541
|
);
|
|
8478
8542
|
}
|
|
8479
|
-
const sessionDir =
|
|
8543
|
+
const sessionDir = path9.join(projectPath, HARNESS_DIR, SESSIONS_DIR, sessionSlug);
|
|
8480
8544
|
if (options?.create) {
|
|
8481
|
-
|
|
8545
|
+
fs12.mkdirSync(sessionDir, { recursive: true });
|
|
8482
8546
|
}
|
|
8483
8547
|
return Ok(sessionDir);
|
|
8484
8548
|
}
|
|
8485
8549
|
function updateSessionIndex(projectPath, sessionSlug, description) {
|
|
8486
|
-
const sessionsDir =
|
|
8487
|
-
|
|
8488
|
-
const indexPath2 =
|
|
8550
|
+
const sessionsDir = path9.join(projectPath, HARNESS_DIR, SESSIONS_DIR);
|
|
8551
|
+
fs12.mkdirSync(sessionsDir, { recursive: true });
|
|
8552
|
+
const indexPath2 = path9.join(sessionsDir, SESSION_INDEX_FILE);
|
|
8489
8553
|
const date = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
|
|
8490
8554
|
const newLine = `- [${sessionSlug}](${sessionSlug}/summary.md) \u2014 ${description} (${date})`;
|
|
8491
|
-
if (!
|
|
8492
|
-
|
|
8555
|
+
if (!fs12.existsSync(indexPath2)) {
|
|
8556
|
+
fs12.writeFileSync(indexPath2, `## Active Sessions
|
|
8493
8557
|
|
|
8494
8558
|
${newLine}
|
|
8495
8559
|
`);
|
|
8496
8560
|
return;
|
|
8497
8561
|
}
|
|
8498
|
-
const content =
|
|
8562
|
+
const content = fs12.readFileSync(indexPath2, "utf-8");
|
|
8499
8563
|
const lines = content.split("\n");
|
|
8500
8564
|
const slugPattern = `- [${sessionSlug}]`;
|
|
8501
8565
|
const existingIdx = lines.findIndex((l) => l.startsWith(slugPattern));
|
|
@@ -8505,7 +8569,7 @@ ${newLine}
|
|
|
8505
8569
|
const lastNonEmpty = lines.reduce((last, line, i) => line.trim() !== "" ? i : last, 0);
|
|
8506
8570
|
lines.splice(lastNonEmpty + 1, 0, newLine);
|
|
8507
8571
|
}
|
|
8508
|
-
|
|
8572
|
+
fs12.writeFileSync(indexPath2, lines.join("\n"));
|
|
8509
8573
|
}
|
|
8510
8574
|
function evictIfNeeded(map) {
|
|
8511
8575
|
if (map.size > MAX_CACHE_ENTRIES) {
|
|
@@ -8518,8 +8582,8 @@ async function getStateDir(projectPath, stream, session) {
|
|
|
8518
8582
|
const sessionResult = resolveSessionDir(projectPath, session, { create: true });
|
|
8519
8583
|
return sessionResult;
|
|
8520
8584
|
}
|
|
8521
|
-
const streamsIndexPath =
|
|
8522
|
-
const hasStreams =
|
|
8585
|
+
const streamsIndexPath = path10.join(projectPath, HARNESS_DIR, "streams", INDEX_FILE);
|
|
8586
|
+
const hasStreams = fs13.existsSync(streamsIndexPath);
|
|
8523
8587
|
if (stream || hasStreams) {
|
|
8524
8588
|
const result = await resolveStreamPath(projectPath, stream ? { stream } : void 0);
|
|
8525
8589
|
if (result.ok) {
|
|
@@ -8529,18 +8593,18 @@ async function getStateDir(projectPath, stream, session) {
|
|
|
8529
8593
|
return result;
|
|
8530
8594
|
}
|
|
8531
8595
|
}
|
|
8532
|
-
return Ok(
|
|
8596
|
+
return Ok(path10.join(projectPath, HARNESS_DIR));
|
|
8533
8597
|
}
|
|
8534
8598
|
async function loadState(projectPath, stream, session) {
|
|
8535
8599
|
try {
|
|
8536
8600
|
const dirResult = await getStateDir(projectPath, stream, session);
|
|
8537
8601
|
if (!dirResult.ok) return dirResult;
|
|
8538
8602
|
const stateDir = dirResult.value;
|
|
8539
|
-
const statePath =
|
|
8540
|
-
if (!
|
|
8603
|
+
const statePath = path11.join(stateDir, STATE_FILE);
|
|
8604
|
+
if (!fs14.existsSync(statePath)) {
|
|
8541
8605
|
return Ok({ ...DEFAULT_STATE });
|
|
8542
8606
|
}
|
|
8543
|
-
const raw =
|
|
8607
|
+
const raw = fs14.readFileSync(statePath, "utf-8");
|
|
8544
8608
|
const parsed = JSON.parse(raw);
|
|
8545
8609
|
const result = HarnessStateSchema.safeParse(parsed);
|
|
8546
8610
|
if (!result.success) {
|
|
@@ -8558,11 +8622,11 @@ async function saveState(projectPath, state, stream, session) {
|
|
|
8558
8622
|
const dirResult = await getStateDir(projectPath, stream, session);
|
|
8559
8623
|
if (!dirResult.ok) return dirResult;
|
|
8560
8624
|
const stateDir = dirResult.value;
|
|
8561
|
-
const statePath =
|
|
8562
|
-
|
|
8625
|
+
const statePath = path11.join(stateDir, STATE_FILE);
|
|
8626
|
+
fs14.mkdirSync(stateDir, { recursive: true });
|
|
8563
8627
|
const tmpPath = statePath + ".tmp";
|
|
8564
|
-
|
|
8565
|
-
|
|
8628
|
+
fs14.writeFileSync(tmpPath, JSON.stringify(state, null, 2));
|
|
8629
|
+
fs14.renameSync(tmpPath, statePath);
|
|
8566
8630
|
return Ok(void 0);
|
|
8567
8631
|
} catch (error) {
|
|
8568
8632
|
return Err(
|
|
@@ -8648,10 +8712,10 @@ function computeContentHash(text) {
|
|
|
8648
8712
|
return crypto2.createHash("sha256").update(text).digest("hex").slice(0, 16);
|
|
8649
8713
|
}
|
|
8650
8714
|
function loadContentHashes(stateDir) {
|
|
8651
|
-
const hashesPath =
|
|
8652
|
-
if (!
|
|
8715
|
+
const hashesPath = path12.join(stateDir, CONTENT_HASHES_FILE);
|
|
8716
|
+
if (!fs15.existsSync(hashesPath)) return {};
|
|
8653
8717
|
try {
|
|
8654
|
-
const raw =
|
|
8718
|
+
const raw = fs15.readFileSync(hashesPath, "utf-8");
|
|
8655
8719
|
const parsed = JSON.parse(raw);
|
|
8656
8720
|
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return {};
|
|
8657
8721
|
return parsed;
|
|
@@ -8660,15 +8724,15 @@ function loadContentHashes(stateDir) {
|
|
|
8660
8724
|
}
|
|
8661
8725
|
}
|
|
8662
8726
|
function saveContentHashes(stateDir, index) {
|
|
8663
|
-
const hashesPath =
|
|
8727
|
+
const hashesPath = path12.join(stateDir, CONTENT_HASHES_FILE);
|
|
8664
8728
|
const tmpPath = hashesPath + ".tmp";
|
|
8665
|
-
|
|
8666
|
-
|
|
8729
|
+
fs15.writeFileSync(tmpPath, JSON.stringify(index, null, 2) + "\n");
|
|
8730
|
+
fs15.renameSync(tmpPath, hashesPath);
|
|
8667
8731
|
}
|
|
8668
8732
|
function rebuildContentHashes(stateDir, learningsFile) {
|
|
8669
|
-
const learningsPath =
|
|
8670
|
-
if (!
|
|
8671
|
-
const content =
|
|
8733
|
+
const learningsPath = path12.join(stateDir, learningsFile);
|
|
8734
|
+
if (!fs15.existsSync(learningsPath)) return {};
|
|
8735
|
+
const content = fs15.readFileSync(learningsPath, "utf-8");
|
|
8672
8736
|
const lines = content.split("\n");
|
|
8673
8737
|
const index = {};
|
|
8674
8738
|
for (let i = 0; i < lines.length; i++) {
|
|
@@ -8727,18 +8791,18 @@ async function loadRelevantLearnings(projectPath, skillName, stream, session) {
|
|
|
8727
8791
|
const dirResult = await getStateDir(projectPath, stream, session);
|
|
8728
8792
|
if (!dirResult.ok) return dirResult;
|
|
8729
8793
|
const stateDir = dirResult.value;
|
|
8730
|
-
const learningsPath =
|
|
8731
|
-
if (!
|
|
8794
|
+
const learningsPath = path13.join(stateDir, LEARNINGS_FILE);
|
|
8795
|
+
if (!fs16.existsSync(learningsPath)) {
|
|
8732
8796
|
return Ok([]);
|
|
8733
8797
|
}
|
|
8734
|
-
const stats =
|
|
8798
|
+
const stats = fs16.statSync(learningsPath);
|
|
8735
8799
|
const cacheKey = learningsPath;
|
|
8736
8800
|
const cached = learningsCacheMap.get(cacheKey);
|
|
8737
8801
|
let entries;
|
|
8738
8802
|
if (cached && cached.mtimeMs === stats.mtimeMs) {
|
|
8739
8803
|
entries = cached.entries;
|
|
8740
8804
|
} else {
|
|
8741
|
-
const content =
|
|
8805
|
+
const content = fs16.readFileSync(learningsPath, "utf-8");
|
|
8742
8806
|
const lines = content.split("\n");
|
|
8743
8807
|
entries = [];
|
|
8744
8808
|
let currentBlock = [];
|
|
@@ -8874,16 +8938,16 @@ function checkOverlap(newEntry, existingEntries, options) {
|
|
|
8874
8938
|
};
|
|
8875
8939
|
}
|
|
8876
8940
|
function sleep(ms) {
|
|
8877
|
-
return new Promise((
|
|
8941
|
+
return new Promise((resolve9) => setTimeout(resolve9, ms));
|
|
8878
8942
|
}
|
|
8879
8943
|
async function acquireFileLock(lockPath) {
|
|
8880
8944
|
for (let attempt = 0; attempt < LOCK_RETRIES; attempt++) {
|
|
8881
8945
|
try {
|
|
8882
|
-
const fd =
|
|
8946
|
+
const fd = fs17.openSync(
|
|
8883
8947
|
lockPath,
|
|
8884
|
-
|
|
8948
|
+
fs17.constants.O_CREAT | fs17.constants.O_EXCL | fs17.constants.O_WRONLY
|
|
8885
8949
|
);
|
|
8886
|
-
|
|
8950
|
+
fs17.closeSync(fd);
|
|
8887
8951
|
return;
|
|
8888
8952
|
} catch (err) {
|
|
8889
8953
|
const isExist = err instanceof Error && "code" in err && err.code === "EEXIST";
|
|
@@ -8897,20 +8961,20 @@ async function acquireFileLock(lockPath) {
|
|
|
8897
8961
|
}
|
|
8898
8962
|
function releaseFileLock(lockPath) {
|
|
8899
8963
|
try {
|
|
8900
|
-
|
|
8964
|
+
fs17.unlinkSync(lockPath);
|
|
8901
8965
|
} catch {
|
|
8902
8966
|
}
|
|
8903
8967
|
}
|
|
8904
8968
|
function loadOrRebuildHashes(stateDir, learningsPath) {
|
|
8905
|
-
const hashesPath =
|
|
8906
|
-
if (
|
|
8969
|
+
const hashesPath = path14.join(stateDir, CONTENT_HASHES_FILE);
|
|
8970
|
+
if (fs17.existsSync(hashesPath)) {
|
|
8907
8971
|
const hashes = loadContentHashes(stateDir);
|
|
8908
|
-
if (Object.keys(hashes).length === 0 &&
|
|
8972
|
+
if (Object.keys(hashes).length === 0 && fs17.existsSync(learningsPath)) {
|
|
8909
8973
|
return rebuildContentHashes(stateDir, LEARNINGS_FILE);
|
|
8910
8974
|
}
|
|
8911
8975
|
return hashes;
|
|
8912
8976
|
}
|
|
8913
|
-
if (
|
|
8977
|
+
if (fs17.existsSync(learningsPath)) {
|
|
8914
8978
|
return rebuildContentHashes(stateDir, LEARNINGS_FILE);
|
|
8915
8979
|
}
|
|
8916
8980
|
return {};
|
|
@@ -8934,8 +8998,8 @@ async function appendLearning(projectPath, learning, skillName, outcome, stream,
|
|
|
8934
8998
|
const dirResult = await getStateDir(projectPath, stream, session);
|
|
8935
8999
|
if (!dirResult.ok) return dirResult;
|
|
8936
9000
|
const stateDir = dirResult.value;
|
|
8937
|
-
const learningsPath =
|
|
8938
|
-
|
|
9001
|
+
const learningsPath = path14.join(stateDir, LEARNINGS_FILE);
|
|
9002
|
+
fs17.mkdirSync(stateDir, { recursive: true });
|
|
8939
9003
|
const lockPath = learningsPath + ".lock";
|
|
8940
9004
|
await acquireFileLock(lockPath);
|
|
8941
9005
|
try {
|
|
@@ -8965,17 +9029,17 @@ ${bulletLine}
|
|
|
8965
9029
|
`;
|
|
8966
9030
|
let overlapResult;
|
|
8967
9031
|
let existingLineCount;
|
|
8968
|
-
if (!
|
|
8969
|
-
|
|
9032
|
+
if (!fs17.existsSync(learningsPath)) {
|
|
9033
|
+
fs17.writeFileSync(learningsPath, `# Learnings
|
|
8970
9034
|
${entry}`);
|
|
8971
9035
|
existingLineCount = 1;
|
|
8972
9036
|
} else {
|
|
8973
|
-
const existingContent =
|
|
9037
|
+
const existingContent = fs17.readFileSync(learningsPath, "utf-8");
|
|
8974
9038
|
const existingEntries = existingContent.split("\n").filter((line) => /^- \*\*\d{4}-\d{2}-\d{2}/.test(line));
|
|
8975
9039
|
const overlap = checkOverlap(bulletLine, existingEntries);
|
|
8976
9040
|
if (overlap.score >= 0.7) overlapResult = overlap;
|
|
8977
9041
|
existingLineCount = existingContent.split("\n").length;
|
|
8978
|
-
|
|
9042
|
+
fs17.appendFileSync(learningsPath, entry);
|
|
8979
9043
|
}
|
|
8980
9044
|
const bulletLineNum = existingLineCount + 2;
|
|
8981
9045
|
contentHashes[contentHash] = { date: timestamp ?? "", line: bulletLineNum };
|
|
@@ -9080,11 +9144,11 @@ async function loadIndexEntries(projectPath, skillName, stream, session) {
|
|
|
9080
9144
|
const dirResult = await getStateDir(projectPath, stream, session);
|
|
9081
9145
|
if (!dirResult.ok) return dirResult;
|
|
9082
9146
|
const stateDir = dirResult.value;
|
|
9083
|
-
const learningsPath =
|
|
9084
|
-
if (!
|
|
9147
|
+
const learningsPath = path14.join(stateDir, LEARNINGS_FILE);
|
|
9148
|
+
if (!fs17.existsSync(learningsPath)) {
|
|
9085
9149
|
return Ok([]);
|
|
9086
9150
|
}
|
|
9087
|
-
const content =
|
|
9151
|
+
const content = fs17.readFileSync(learningsPath, "utf-8");
|
|
9088
9152
|
const lines = content.split("\n");
|
|
9089
9153
|
const indexEntries = [];
|
|
9090
9154
|
let pendingFrontmatter = null;
|
|
@@ -9126,16 +9190,16 @@ async function archiveLearnings(projectPath, entries, stream) {
|
|
|
9126
9190
|
const dirResult = await getStateDir(projectPath, stream);
|
|
9127
9191
|
if (!dirResult.ok) return dirResult;
|
|
9128
9192
|
const stateDir = dirResult.value;
|
|
9129
|
-
const archiveDir =
|
|
9130
|
-
|
|
9193
|
+
const archiveDir = path15.join(stateDir, "learnings-archive");
|
|
9194
|
+
fs18.mkdirSync(archiveDir, { recursive: true });
|
|
9131
9195
|
const now = /* @__PURE__ */ new Date();
|
|
9132
9196
|
const yearMonth = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, "0")}`;
|
|
9133
|
-
const archivePath =
|
|
9197
|
+
const archivePath = path15.join(archiveDir, `${yearMonth}.md`);
|
|
9134
9198
|
const archiveContent = entries.join("\n\n") + "\n";
|
|
9135
|
-
if (
|
|
9136
|
-
|
|
9199
|
+
if (fs18.existsSync(archivePath)) {
|
|
9200
|
+
fs18.appendFileSync(archivePath, "\n" + archiveContent);
|
|
9137
9201
|
} else {
|
|
9138
|
-
|
|
9202
|
+
fs18.writeFileSync(archivePath, `# Learnings Archive
|
|
9139
9203
|
|
|
9140
9204
|
${archiveContent}`);
|
|
9141
9205
|
}
|
|
@@ -9153,8 +9217,8 @@ async function pruneLearnings(projectPath, stream) {
|
|
|
9153
9217
|
const dirResult = await getStateDir(projectPath, stream);
|
|
9154
9218
|
if (!dirResult.ok) return dirResult;
|
|
9155
9219
|
const stateDir = dirResult.value;
|
|
9156
|
-
const learningsPath =
|
|
9157
|
-
if (!
|
|
9220
|
+
const learningsPath = path15.join(stateDir, LEARNINGS_FILE);
|
|
9221
|
+
if (!fs18.existsSync(learningsPath)) {
|
|
9158
9222
|
return Ok({ kept: 0, archived: 0, patterns: [] });
|
|
9159
9223
|
}
|
|
9160
9224
|
const loadResult = await loadRelevantLearnings(projectPath, void 0, stream);
|
|
@@ -9185,7 +9249,7 @@ async function pruneLearnings(projectPath, stream) {
|
|
|
9185
9249
|
if (!archiveResult.ok) return archiveResult;
|
|
9186
9250
|
}
|
|
9187
9251
|
const newContent = "# Learnings\n\n" + toKeep.join("\n\n") + "\n";
|
|
9188
|
-
|
|
9252
|
+
fs18.writeFileSync(learningsPath, newContent);
|
|
9189
9253
|
invalidateLearningsCacheEntry(learningsPath);
|
|
9190
9254
|
return Ok({
|
|
9191
9255
|
kept: toKeep.length,
|
|
@@ -9229,19 +9293,19 @@ async function promoteSessionLearnings(projectPath, sessionSlug, stream) {
|
|
|
9229
9293
|
const dirResult = await getStateDir(projectPath, stream);
|
|
9230
9294
|
if (!dirResult.ok) return dirResult;
|
|
9231
9295
|
const stateDir = dirResult.value;
|
|
9232
|
-
const globalPath =
|
|
9233
|
-
const existingGlobal =
|
|
9296
|
+
const globalPath = path15.join(stateDir, LEARNINGS_FILE);
|
|
9297
|
+
const existingGlobal = fs18.existsSync(globalPath) ? fs18.readFileSync(globalPath, "utf-8") : "";
|
|
9234
9298
|
const newEntries = toPromote.filter((entry) => !existingGlobal.includes(entry.trim()));
|
|
9235
9299
|
if (newEntries.length === 0) {
|
|
9236
9300
|
return Ok({ promoted: 0, skipped: skipped + toPromote.length });
|
|
9237
9301
|
}
|
|
9238
9302
|
const promotedContent = newEntries.join("\n\n") + "\n";
|
|
9239
9303
|
if (!existingGlobal) {
|
|
9240
|
-
|
|
9304
|
+
fs18.writeFileSync(globalPath, `# Learnings
|
|
9241
9305
|
|
|
9242
9306
|
${promotedContent}`);
|
|
9243
9307
|
} else {
|
|
9244
|
-
|
|
9308
|
+
fs18.appendFileSync(globalPath, "\n\n" + promotedContent);
|
|
9245
9309
|
}
|
|
9246
9310
|
invalidateLearningsCacheEntry(globalPath);
|
|
9247
9311
|
return Ok({
|
|
@@ -9277,13 +9341,13 @@ async function detectStaleLearnings(projectPath, stream, session) {
|
|
|
9277
9341
|
freshCount++;
|
|
9278
9342
|
continue;
|
|
9279
9343
|
}
|
|
9280
|
-
const resolvedRoot =
|
|
9344
|
+
const resolvedRoot = path16.resolve(projectPath);
|
|
9281
9345
|
const missing = refs.filter((ref) => {
|
|
9282
|
-
const absPath =
|
|
9283
|
-
if (!absPath.startsWith(resolvedRoot +
|
|
9346
|
+
const absPath = path16.resolve(projectPath, ref);
|
|
9347
|
+
if (!absPath.startsWith(resolvedRoot + path16.sep) && absPath !== resolvedRoot) {
|
|
9284
9348
|
return false;
|
|
9285
9349
|
}
|
|
9286
|
-
return !
|
|
9350
|
+
return !fs19.existsSync(absPath);
|
|
9287
9351
|
});
|
|
9288
9352
|
if (missing.length > 0) {
|
|
9289
9353
|
const idx = extractIndexEntry(entry);
|
|
@@ -9318,17 +9382,17 @@ async function appendFailure(projectPath, description, skillName, type, stream,
|
|
|
9318
9382
|
const dirResult = await getStateDir(projectPath, stream, session);
|
|
9319
9383
|
if (!dirResult.ok) return dirResult;
|
|
9320
9384
|
const stateDir = dirResult.value;
|
|
9321
|
-
const failuresPath =
|
|
9322
|
-
|
|
9385
|
+
const failuresPath = path17.join(stateDir, FAILURES_FILE);
|
|
9386
|
+
fs20.mkdirSync(stateDir, { recursive: true });
|
|
9323
9387
|
const timestamp = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
|
|
9324
9388
|
const entry = `
|
|
9325
9389
|
- **${timestamp} [skill:${skillName}] [type:${type}]:** ${description}
|
|
9326
9390
|
`;
|
|
9327
|
-
if (!
|
|
9328
|
-
|
|
9391
|
+
if (!fs20.existsSync(failuresPath)) {
|
|
9392
|
+
fs20.writeFileSync(failuresPath, `# Failures
|
|
9329
9393
|
${entry}`);
|
|
9330
9394
|
} else {
|
|
9331
|
-
|
|
9395
|
+
fs20.appendFileSync(failuresPath, entry);
|
|
9332
9396
|
}
|
|
9333
9397
|
failuresCacheMap.delete(failuresPath);
|
|
9334
9398
|
return Ok(void 0);
|
|
@@ -9345,17 +9409,17 @@ async function loadFailures(projectPath, stream, session) {
|
|
|
9345
9409
|
const dirResult = await getStateDir(projectPath, stream, session);
|
|
9346
9410
|
if (!dirResult.ok) return dirResult;
|
|
9347
9411
|
const stateDir = dirResult.value;
|
|
9348
|
-
const failuresPath =
|
|
9349
|
-
if (!
|
|
9412
|
+
const failuresPath = path17.join(stateDir, FAILURES_FILE);
|
|
9413
|
+
if (!fs20.existsSync(failuresPath)) {
|
|
9350
9414
|
return Ok([]);
|
|
9351
9415
|
}
|
|
9352
|
-
const stats =
|
|
9416
|
+
const stats = fs20.statSync(failuresPath);
|
|
9353
9417
|
const cacheKey = failuresPath;
|
|
9354
9418
|
const cached = failuresCacheMap.get(cacheKey);
|
|
9355
9419
|
if (cached && cached.mtimeMs === stats.mtimeMs) {
|
|
9356
9420
|
return Ok(cached.entries);
|
|
9357
9421
|
}
|
|
9358
|
-
const content =
|
|
9422
|
+
const content = fs20.readFileSync(failuresPath, "utf-8");
|
|
9359
9423
|
const entries = [];
|
|
9360
9424
|
for (const line of content.split("\n")) {
|
|
9361
9425
|
const match = line.match(FAILURE_LINE_REGEX);
|
|
@@ -9384,20 +9448,20 @@ async function archiveFailures(projectPath, stream, session) {
|
|
|
9384
9448
|
const dirResult = await getStateDir(projectPath, stream, session);
|
|
9385
9449
|
if (!dirResult.ok) return dirResult;
|
|
9386
9450
|
const stateDir = dirResult.value;
|
|
9387
|
-
const failuresPath =
|
|
9388
|
-
if (!
|
|
9451
|
+
const failuresPath = path17.join(stateDir, FAILURES_FILE);
|
|
9452
|
+
if (!fs20.existsSync(failuresPath)) {
|
|
9389
9453
|
return Ok(void 0);
|
|
9390
9454
|
}
|
|
9391
|
-
const archiveDir =
|
|
9392
|
-
|
|
9455
|
+
const archiveDir = path17.join(stateDir, "archive");
|
|
9456
|
+
fs20.mkdirSync(archiveDir, { recursive: true });
|
|
9393
9457
|
const date = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
|
|
9394
9458
|
let archiveName = `failures-${date}.md`;
|
|
9395
9459
|
let counter = 2;
|
|
9396
|
-
while (
|
|
9460
|
+
while (fs20.existsSync(path17.join(archiveDir, archiveName))) {
|
|
9397
9461
|
archiveName = `failures-${date}-${counter}.md`;
|
|
9398
9462
|
counter++;
|
|
9399
9463
|
}
|
|
9400
|
-
|
|
9464
|
+
fs20.renameSync(failuresPath, path17.join(archiveDir, archiveName));
|
|
9401
9465
|
failuresCacheMap.delete(failuresPath);
|
|
9402
9466
|
return Ok(void 0);
|
|
9403
9467
|
} catch (error) {
|
|
@@ -9413,11 +9477,11 @@ async function saveHandoff(projectPath, handoff, stream, session) {
|
|
|
9413
9477
|
const dirResult = await getStateDir(projectPath, stream, session);
|
|
9414
9478
|
if (!dirResult.ok) return dirResult;
|
|
9415
9479
|
const stateDir = dirResult.value;
|
|
9416
|
-
const handoffPath =
|
|
9417
|
-
|
|
9480
|
+
const handoffPath = path18.join(stateDir, HANDOFF_FILE);
|
|
9481
|
+
fs21.mkdirSync(stateDir, { recursive: true });
|
|
9418
9482
|
const tmpPath = handoffPath + ".tmp";
|
|
9419
|
-
|
|
9420
|
-
|
|
9483
|
+
fs21.writeFileSync(tmpPath, JSON.stringify(handoff, null, 2));
|
|
9484
|
+
fs21.renameSync(tmpPath, handoffPath);
|
|
9421
9485
|
return Ok(void 0);
|
|
9422
9486
|
} catch (error) {
|
|
9423
9487
|
return Err(
|
|
@@ -9430,11 +9494,11 @@ async function loadHandoff(projectPath, stream, session) {
|
|
|
9430
9494
|
const dirResult = await getStateDir(projectPath, stream, session);
|
|
9431
9495
|
if (!dirResult.ok) return dirResult;
|
|
9432
9496
|
const stateDir = dirResult.value;
|
|
9433
|
-
const handoffPath =
|
|
9434
|
-
if (!
|
|
9497
|
+
const handoffPath = path18.join(stateDir, HANDOFF_FILE);
|
|
9498
|
+
if (!fs21.existsSync(handoffPath)) {
|
|
9435
9499
|
return Ok(null);
|
|
9436
9500
|
}
|
|
9437
|
-
const raw =
|
|
9501
|
+
const raw = fs21.readFileSync(handoffPath, "utf-8");
|
|
9438
9502
|
const parsed = JSON.parse(raw);
|
|
9439
9503
|
const result = HandoffSchema.safeParse(parsed);
|
|
9440
9504
|
if (!result.success) {
|
|
@@ -9448,28 +9512,28 @@ async function loadHandoff(projectPath, stream, session) {
|
|
|
9448
9512
|
}
|
|
9449
9513
|
}
|
|
9450
9514
|
function loadChecksFromConfig(gateConfigPath) {
|
|
9451
|
-
if (!
|
|
9452
|
-
const raw = JSON.parse(
|
|
9515
|
+
if (!fs22.existsSync(gateConfigPath)) return [];
|
|
9516
|
+
const raw = JSON.parse(fs22.readFileSync(gateConfigPath, "utf-8"));
|
|
9453
9517
|
const config = GateConfigSchema.safeParse(raw);
|
|
9454
9518
|
if (config.success && config.data.checks) return config.data.checks;
|
|
9455
9519
|
return [];
|
|
9456
9520
|
}
|
|
9457
9521
|
function discoverChecksFromProject(projectPath) {
|
|
9458
9522
|
const checks = [];
|
|
9459
|
-
const packageJsonPath =
|
|
9460
|
-
if (
|
|
9461
|
-
const pkg = JSON.parse(
|
|
9523
|
+
const packageJsonPath = path19.join(projectPath, "package.json");
|
|
9524
|
+
if (fs22.existsSync(packageJsonPath)) {
|
|
9525
|
+
const pkg = JSON.parse(fs22.readFileSync(packageJsonPath, "utf-8"));
|
|
9462
9526
|
const scripts = pkg.scripts || {};
|
|
9463
9527
|
if (scripts.test) checks.push({ name: "test", command: "npm test" });
|
|
9464
9528
|
if (scripts.lint) checks.push({ name: "lint", command: "npm run lint" });
|
|
9465
9529
|
if (scripts.typecheck) checks.push({ name: "typecheck", command: "npm run typecheck" });
|
|
9466
9530
|
if (scripts.build) checks.push({ name: "build", command: "npm run build" });
|
|
9467
9531
|
}
|
|
9468
|
-
if (
|
|
9532
|
+
if (fs22.existsSync(path19.join(projectPath, "go.mod"))) {
|
|
9469
9533
|
checks.push({ name: "test", command: "go test ./..." });
|
|
9470
9534
|
checks.push({ name: "build", command: "go build ./..." });
|
|
9471
9535
|
}
|
|
9472
|
-
if (
|
|
9536
|
+
if (fs22.existsSync(path19.join(projectPath, "pyproject.toml")) || fs22.existsSync(path19.join(projectPath, "setup.py"))) {
|
|
9473
9537
|
checks.push({ name: "test", command: "python -m pytest" });
|
|
9474
9538
|
}
|
|
9475
9539
|
return checks;
|
|
@@ -9509,8 +9573,8 @@ function executeCheck(check, projectPath) {
|
|
|
9509
9573
|
}
|
|
9510
9574
|
}
|
|
9511
9575
|
async function runMechanicalGate(projectPath) {
|
|
9512
|
-
const harnessDir =
|
|
9513
|
-
const gateConfigPath =
|
|
9576
|
+
const harnessDir = path19.join(projectPath, HARNESS_DIR);
|
|
9577
|
+
const gateConfigPath = path19.join(harnessDir, GATE_CONFIG_FILE);
|
|
9514
9578
|
try {
|
|
9515
9579
|
let checks = loadChecksFromConfig(gateConfigPath);
|
|
9516
9580
|
if (checks.length === 0) {
|
|
@@ -9566,9 +9630,9 @@ function writeSessionSummary(projectPath, sessionSlug, data) {
|
|
|
9566
9630
|
const dirResult = resolveSessionDir(projectPath, sessionSlug, { create: true });
|
|
9567
9631
|
if (!dirResult.ok) return dirResult;
|
|
9568
9632
|
const sessionDir = dirResult.value;
|
|
9569
|
-
const summaryPath =
|
|
9633
|
+
const summaryPath = path20.join(sessionDir, SUMMARY_FILE);
|
|
9570
9634
|
const content = formatSummary(data);
|
|
9571
|
-
|
|
9635
|
+
fs23.writeFileSync(summaryPath, content);
|
|
9572
9636
|
const description = deriveIndexDescription(data);
|
|
9573
9637
|
updateSessionIndex(projectPath, sessionSlug, description);
|
|
9574
9638
|
return Ok(void 0);
|
|
@@ -9585,11 +9649,11 @@ function loadSessionSummary(projectPath, sessionSlug) {
|
|
|
9585
9649
|
const dirResult = resolveSessionDir(projectPath, sessionSlug);
|
|
9586
9650
|
if (!dirResult.ok) return dirResult;
|
|
9587
9651
|
const sessionDir = dirResult.value;
|
|
9588
|
-
const summaryPath =
|
|
9589
|
-
if (!
|
|
9652
|
+
const summaryPath = path20.join(sessionDir, SUMMARY_FILE);
|
|
9653
|
+
if (!fs23.existsSync(summaryPath)) {
|
|
9590
9654
|
return Ok(null);
|
|
9591
9655
|
}
|
|
9592
|
-
const content =
|
|
9656
|
+
const content = fs23.readFileSync(summaryPath, "utf-8");
|
|
9593
9657
|
return Ok(content);
|
|
9594
9658
|
} catch (error) {
|
|
9595
9659
|
return Err(
|
|
@@ -9601,11 +9665,11 @@ function loadSessionSummary(projectPath, sessionSlug) {
|
|
|
9601
9665
|
}
|
|
9602
9666
|
function listActiveSessions(projectPath) {
|
|
9603
9667
|
try {
|
|
9604
|
-
const indexPath2 =
|
|
9605
|
-
if (!
|
|
9668
|
+
const indexPath2 = path20.join(projectPath, HARNESS_DIR, SESSIONS_DIR, SESSION_INDEX_FILE);
|
|
9669
|
+
if (!fs23.existsSync(indexPath2)) {
|
|
9606
9670
|
return Ok(null);
|
|
9607
9671
|
}
|
|
9608
|
-
const content =
|
|
9672
|
+
const content = fs23.readFileSync(indexPath2, "utf-8");
|
|
9609
9673
|
return Ok(content);
|
|
9610
9674
|
} catch (error) {
|
|
9611
9675
|
return Err(
|
|
@@ -9626,12 +9690,12 @@ async function loadSessionState(projectPath, sessionSlug) {
|
|
|
9626
9690
|
const dirResult = resolveSessionDir(projectPath, sessionSlug);
|
|
9627
9691
|
if (!dirResult.ok) return dirResult;
|
|
9628
9692
|
const sessionDir = dirResult.value;
|
|
9629
|
-
const filePath =
|
|
9630
|
-
if (!
|
|
9693
|
+
const filePath = path21.join(sessionDir, SESSION_STATE_FILE);
|
|
9694
|
+
if (!fs24.existsSync(filePath)) {
|
|
9631
9695
|
return Ok(emptySections());
|
|
9632
9696
|
}
|
|
9633
9697
|
try {
|
|
9634
|
-
const raw =
|
|
9698
|
+
const raw = fs24.readFileSync(filePath, "utf-8");
|
|
9635
9699
|
const parsed = JSON.parse(raw);
|
|
9636
9700
|
const sections = emptySections();
|
|
9637
9701
|
for (const name of SESSION_SECTION_NAMES) {
|
|
@@ -9652,9 +9716,9 @@ async function saveSessionState(projectPath, sessionSlug, sections) {
|
|
|
9652
9716
|
const dirResult = resolveSessionDir(projectPath, sessionSlug, { create: true });
|
|
9653
9717
|
if (!dirResult.ok) return dirResult;
|
|
9654
9718
|
const sessionDir = dirResult.value;
|
|
9655
|
-
const filePath =
|
|
9719
|
+
const filePath = path21.join(sessionDir, SESSION_STATE_FILE);
|
|
9656
9720
|
try {
|
|
9657
|
-
|
|
9721
|
+
fs24.writeFileSync(filePath, JSON.stringify(sections, null, 2));
|
|
9658
9722
|
return Ok(void 0);
|
|
9659
9723
|
} catch (error) {
|
|
9660
9724
|
return Err(
|
|
@@ -9710,26 +9774,26 @@ async function archiveSession(projectPath, sessionSlug) {
|
|
|
9710
9774
|
const dirResult = resolveSessionDir(projectPath, sessionSlug);
|
|
9711
9775
|
if (!dirResult.ok) return dirResult;
|
|
9712
9776
|
const sessionDir = dirResult.value;
|
|
9713
|
-
if (!
|
|
9777
|
+
if (!fs25.existsSync(sessionDir)) {
|
|
9714
9778
|
return Err(new Error(`Session '${sessionSlug}' not found at ${sessionDir}`));
|
|
9715
9779
|
}
|
|
9716
|
-
const archiveBase =
|
|
9780
|
+
const archiveBase = path22.join(projectPath, HARNESS_DIR, ARCHIVE_DIR, "sessions");
|
|
9717
9781
|
try {
|
|
9718
|
-
|
|
9782
|
+
fs25.mkdirSync(archiveBase, { recursive: true });
|
|
9719
9783
|
const date = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
|
|
9720
9784
|
let archiveName = `${sessionSlug}-${date}`;
|
|
9721
9785
|
let counter = 1;
|
|
9722
|
-
while (
|
|
9786
|
+
while (fs25.existsSync(path22.join(archiveBase, archiveName))) {
|
|
9723
9787
|
archiveName = `${sessionSlug}-${date}-${counter}`;
|
|
9724
9788
|
counter++;
|
|
9725
9789
|
}
|
|
9726
|
-
const dest =
|
|
9790
|
+
const dest = path22.join(archiveBase, archiveName);
|
|
9727
9791
|
try {
|
|
9728
|
-
|
|
9792
|
+
fs25.renameSync(sessionDir, dest);
|
|
9729
9793
|
} catch (renameErr) {
|
|
9730
9794
|
if (renameErr instanceof Error && "code" in renameErr && renameErr.code === "EXDEV") {
|
|
9731
|
-
|
|
9732
|
-
|
|
9795
|
+
fs25.cpSync(sessionDir, dest, { recursive: true });
|
|
9796
|
+
fs25.rmSync(sessionDir, { recursive: true });
|
|
9733
9797
|
} else {
|
|
9734
9798
|
throw renameErr;
|
|
9735
9799
|
}
|
|
@@ -9751,8 +9815,8 @@ function loadKnownHashes(eventsPath) {
|
|
|
9751
9815
|
const cached = knownHashesCache.get(eventsPath);
|
|
9752
9816
|
if (cached) return cached;
|
|
9753
9817
|
const hashes = /* @__PURE__ */ new Set();
|
|
9754
|
-
if (
|
|
9755
|
-
const content =
|
|
9818
|
+
if (fs26.existsSync(eventsPath)) {
|
|
9819
|
+
const content = fs26.readFileSync(eventsPath, "utf-8");
|
|
9756
9820
|
const lines = content.split("\n").filter((line) => line.trim() !== "");
|
|
9757
9821
|
for (const line of lines) {
|
|
9758
9822
|
try {
|
|
@@ -9775,8 +9839,8 @@ async function emitEvent(projectPath, event, options) {
|
|
|
9775
9839
|
const dirResult = await getStateDir(projectPath, options?.stream, options?.session);
|
|
9776
9840
|
if (!dirResult.ok) return dirResult;
|
|
9777
9841
|
const stateDir = dirResult.value;
|
|
9778
|
-
const eventsPath =
|
|
9779
|
-
|
|
9842
|
+
const eventsPath = path23.join(stateDir, EVENTS_FILE);
|
|
9843
|
+
fs26.mkdirSync(stateDir, { recursive: true });
|
|
9780
9844
|
const contentHash = computeEventHash(event, options?.session);
|
|
9781
9845
|
const knownHashes = loadKnownHashes(eventsPath);
|
|
9782
9846
|
if (knownHashes.has(contentHash)) {
|
|
@@ -9790,7 +9854,7 @@ async function emitEvent(projectPath, event, options) {
|
|
|
9790
9854
|
if (options?.session) {
|
|
9791
9855
|
fullEvent.session = options.session;
|
|
9792
9856
|
}
|
|
9793
|
-
|
|
9857
|
+
fs26.appendFileSync(eventsPath, JSON.stringify(fullEvent) + "\n");
|
|
9794
9858
|
knownHashes.add(contentHash);
|
|
9795
9859
|
return Ok({ written: true });
|
|
9796
9860
|
} catch (error) {
|
|
@@ -9804,11 +9868,11 @@ async function loadEvents(projectPath, options) {
|
|
|
9804
9868
|
const dirResult = await getStateDir(projectPath, options?.stream, options?.session);
|
|
9805
9869
|
if (!dirResult.ok) return dirResult;
|
|
9806
9870
|
const stateDir = dirResult.value;
|
|
9807
|
-
const eventsPath =
|
|
9808
|
-
if (!
|
|
9871
|
+
const eventsPath = path23.join(stateDir, EVENTS_FILE);
|
|
9872
|
+
if (!fs26.existsSync(eventsPath)) {
|
|
9809
9873
|
return Ok([]);
|
|
9810
9874
|
}
|
|
9811
|
-
const content =
|
|
9875
|
+
const content = fs26.readFileSync(eventsPath, "utf-8");
|
|
9812
9876
|
const lines = content.split("\n").filter((line) => line.trim() !== "");
|
|
9813
9877
|
const events = [];
|
|
9814
9878
|
for (const line of lines) {
|
|
@@ -10057,11 +10121,11 @@ function nodeSubStacks(allDeps) {
|
|
|
10057
10121
|
return found;
|
|
10058
10122
|
}
|
|
10059
10123
|
function detectNodeStacks(projectRoot) {
|
|
10060
|
-
const pkgJsonPath =
|
|
10061
|
-
if (!
|
|
10124
|
+
const pkgJsonPath = path24.join(projectRoot, "package.json");
|
|
10125
|
+
if (!fs27.existsSync(pkgJsonPath)) return [];
|
|
10062
10126
|
const stacks = ["node"];
|
|
10063
10127
|
try {
|
|
10064
|
-
const pkgJson = JSON.parse(
|
|
10128
|
+
const pkgJson = JSON.parse(fs27.readFileSync(pkgJsonPath, "utf-8"));
|
|
10065
10129
|
const allDeps = { ...pkgJson.dependencies, ...pkgJson.devDependencies };
|
|
10066
10130
|
stacks.push(...nodeSubStacks(allDeps));
|
|
10067
10131
|
} catch {
|
|
@@ -10070,10 +10134,10 @@ function detectNodeStacks(projectRoot) {
|
|
|
10070
10134
|
}
|
|
10071
10135
|
function detectStack(projectRoot) {
|
|
10072
10136
|
const stacks = [...detectNodeStacks(projectRoot)];
|
|
10073
|
-
if (
|
|
10137
|
+
if (fs27.existsSync(path24.join(projectRoot, "go.mod"))) {
|
|
10074
10138
|
stacks.push("go");
|
|
10075
10139
|
}
|
|
10076
|
-
const hasPython =
|
|
10140
|
+
const hasPython = fs27.existsSync(path24.join(projectRoot, "requirements.txt")) || fs27.existsSync(path24.join(projectRoot, "pyproject.toml"));
|
|
10077
10141
|
if (hasPython) {
|
|
10078
10142
|
stacks.push("python");
|
|
10079
10143
|
}
|
|
@@ -10116,7 +10180,7 @@ function getInjectionPatterns() {
|
|
|
10116
10180
|
}
|
|
10117
10181
|
function getTaintFilePath(projectRoot, sessionId) {
|
|
10118
10182
|
const id = sessionId || DEFAULT_SESSION_ID;
|
|
10119
|
-
return
|
|
10183
|
+
return join392(projectRoot, ".harness", `session-taint-${id}.json`);
|
|
10120
10184
|
}
|
|
10121
10185
|
function readTaint(projectRoot, sessionId) {
|
|
10122
10186
|
const filePath = getTaintFilePath(projectRoot, sessionId);
|
|
@@ -10198,14 +10262,14 @@ function clearTaint(projectRoot, sessionId) {
|
|
|
10198
10262
|
return 0;
|
|
10199
10263
|
}
|
|
10200
10264
|
}
|
|
10201
|
-
const harnessDir =
|
|
10265
|
+
const harnessDir = join392(projectRoot, ".harness");
|
|
10202
10266
|
let count = 0;
|
|
10203
10267
|
try {
|
|
10204
10268
|
const files = readdirSync3(harnessDir);
|
|
10205
10269
|
for (const file of files) {
|
|
10206
10270
|
if (file.startsWith("session-taint-") && file.endsWith(".json")) {
|
|
10207
10271
|
try {
|
|
10208
|
-
unlinkSync2(
|
|
10272
|
+
unlinkSync2(join392(harnessDir, file));
|
|
10209
10273
|
count++;
|
|
10210
10274
|
} catch {
|
|
10211
10275
|
}
|
|
@@ -10216,7 +10280,7 @@ function clearTaint(projectRoot, sessionId) {
|
|
|
10216
10280
|
return count;
|
|
10217
10281
|
}
|
|
10218
10282
|
function listTaintedSessions(projectRoot) {
|
|
10219
|
-
const harnessDir =
|
|
10283
|
+
const harnessDir = join392(projectRoot, ".harness");
|
|
10220
10284
|
const sessions = [];
|
|
10221
10285
|
try {
|
|
10222
10286
|
const files = readdirSync3(harnessDir);
|
|
@@ -10288,7 +10352,7 @@ function securityFindingId(finding) {
|
|
|
10288
10352
|
}
|
|
10289
10353
|
async function runValidateCheck(projectRoot, config) {
|
|
10290
10354
|
const issues = [];
|
|
10291
|
-
const agentsPath =
|
|
10355
|
+
const agentsPath = path25.join(projectRoot, config.agentsMapPath ?? "AGENTS.md");
|
|
10292
10356
|
const result = await validateAgentsMap(agentsPath);
|
|
10293
10357
|
if (!result.ok) {
|
|
10294
10358
|
issues.push({ severity: "error", message: result.error.message });
|
|
@@ -10345,7 +10409,7 @@ async function runDepsCheck(projectRoot, config) {
|
|
|
10345
10409
|
}
|
|
10346
10410
|
async function runDocsCheck(projectRoot, config) {
|
|
10347
10411
|
const issues = [];
|
|
10348
|
-
const docsDir =
|
|
10412
|
+
const docsDir = path25.join(projectRoot, config.docsDir ?? "docs");
|
|
10349
10413
|
const entropyConfig = config.entropy || {};
|
|
10350
10414
|
const result = await checkDocCoverage("project", {
|
|
10351
10415
|
docsDir,
|
|
@@ -10530,7 +10594,7 @@ async function runTraceabilityCheck(projectRoot, config) {
|
|
|
10530
10594
|
const issues = [];
|
|
10531
10595
|
const traceConfig = config.traceability || {};
|
|
10532
10596
|
if (traceConfig.enabled === false) return issues;
|
|
10533
|
-
const graphDir =
|
|
10597
|
+
const graphDir = path25.join(projectRoot, ".harness", "graph");
|
|
10534
10598
|
const store = new GraphStore();
|
|
10535
10599
|
const loaded = await store.load(graphDir);
|
|
10536
10600
|
if (!loaded) {
|
|
@@ -10708,7 +10772,7 @@ async function runMechanicalChecks(options) {
|
|
|
10708
10772
|
};
|
|
10709
10773
|
if (!skip.includes("validate")) {
|
|
10710
10774
|
try {
|
|
10711
|
-
const agentsPath =
|
|
10775
|
+
const agentsPath = path26.join(projectRoot, config.agentsMapPath ?? "AGENTS.md");
|
|
10712
10776
|
const result = await validateAgentsMap(agentsPath);
|
|
10713
10777
|
if (!result.ok) {
|
|
10714
10778
|
statuses.validate = "fail";
|
|
@@ -10745,7 +10809,7 @@ async function runMechanicalChecks(options) {
|
|
|
10745
10809
|
statuses.validate = "fail";
|
|
10746
10810
|
findings.push({
|
|
10747
10811
|
tool: "validate",
|
|
10748
|
-
file:
|
|
10812
|
+
file: path26.join(projectRoot, "AGENTS.md"),
|
|
10749
10813
|
message: err instanceof Error ? err.message : String(err),
|
|
10750
10814
|
severity: "error"
|
|
10751
10815
|
});
|
|
@@ -10809,7 +10873,7 @@ async function runMechanicalChecks(options) {
|
|
|
10809
10873
|
(async () => {
|
|
10810
10874
|
const localFindings = [];
|
|
10811
10875
|
try {
|
|
10812
|
-
const docsDir =
|
|
10876
|
+
const docsDir = path26.join(projectRoot, config.docsDir ?? "docs");
|
|
10813
10877
|
const result = await checkDocCoverage("project", { docsDir });
|
|
10814
10878
|
if (!result.ok) {
|
|
10815
10879
|
statuses["check-docs"] = "warn";
|
|
@@ -10836,7 +10900,7 @@ async function runMechanicalChecks(options) {
|
|
|
10836
10900
|
statuses["check-docs"] = "warn";
|
|
10837
10901
|
localFindings.push({
|
|
10838
10902
|
tool: "check-docs",
|
|
10839
|
-
file:
|
|
10903
|
+
file: path26.join(projectRoot, "docs"),
|
|
10840
10904
|
message: err instanceof Error ? err.message : String(err),
|
|
10841
10905
|
severity: "warning"
|
|
10842
10906
|
});
|
|
@@ -10933,18 +10997,18 @@ function computeContextBudget(diffLines) {
|
|
|
10933
10997
|
return diffLines;
|
|
10934
10998
|
}
|
|
10935
10999
|
function isWithinProject(absPath, projectRoot) {
|
|
10936
|
-
const resolvedRoot =
|
|
10937
|
-
const resolvedPath =
|
|
10938
|
-
return resolvedPath.startsWith(resolvedRoot) || resolvedPath ===
|
|
11000
|
+
const resolvedRoot = path27.resolve(projectRoot) + path27.sep;
|
|
11001
|
+
const resolvedPath = path27.resolve(absPath);
|
|
11002
|
+
return resolvedPath.startsWith(resolvedRoot) || resolvedPath === path27.resolve(projectRoot);
|
|
10939
11003
|
}
|
|
10940
11004
|
async function readContextFile(projectRoot, filePath, reason) {
|
|
10941
|
-
const absPath =
|
|
11005
|
+
const absPath = path27.isAbsolute(filePath) ? filePath : path27.join(projectRoot, filePath);
|
|
10942
11006
|
if (!isWithinProject(absPath, projectRoot)) return null;
|
|
10943
11007
|
const result = await readFileContent(absPath);
|
|
10944
11008
|
if (!result.ok) return null;
|
|
10945
11009
|
const content = result.value;
|
|
10946
11010
|
const lines = content.split("\n").length;
|
|
10947
|
-
const relPath2 =
|
|
11011
|
+
const relPath2 = path27.isAbsolute(filePath) ? relativePosix(projectRoot, filePath) : filePath;
|
|
10948
11012
|
return { path: relPath2, content, reason, lines };
|
|
10949
11013
|
}
|
|
10950
11014
|
function extractImportSources2(content) {
|
|
@@ -10959,24 +11023,24 @@ function extractImportSources2(content) {
|
|
|
10959
11023
|
}
|
|
10960
11024
|
async function resolveImportPath2(projectRoot, fromFile, importSource) {
|
|
10961
11025
|
if (!importSource.startsWith(".")) return null;
|
|
10962
|
-
const fromDir =
|
|
10963
|
-
const basePath =
|
|
11026
|
+
const fromDir = path27.dirname(path27.join(projectRoot, fromFile));
|
|
11027
|
+
const basePath = path27.resolve(fromDir, importSource);
|
|
10964
11028
|
if (!isWithinProject(basePath, projectRoot)) return null;
|
|
10965
11029
|
const relBase = relativePosix(projectRoot, basePath);
|
|
10966
|
-
const sourceExt =
|
|
11030
|
+
const sourceExt = path27.extname(relBase);
|
|
10967
11031
|
const candidates = [];
|
|
10968
11032
|
const fallbacks = JS_EXT_FALLBACKS22[sourceExt];
|
|
10969
11033
|
if (fallbacks) {
|
|
10970
11034
|
const stripped = relBase.slice(0, -sourceExt.length);
|
|
10971
11035
|
for (const ext of fallbacks) candidates.push(stripped + ext);
|
|
10972
|
-
candidates.push(
|
|
10973
|
-
candidates.push(
|
|
10974
|
-
candidates.push(
|
|
11036
|
+
candidates.push(path27.join(stripped, "index.ts"));
|
|
11037
|
+
candidates.push(path27.join(stripped, "index.tsx"));
|
|
11038
|
+
candidates.push(path27.join(stripped, "index.jsx"));
|
|
10975
11039
|
}
|
|
10976
11040
|
candidates.push(relBase + ".ts", relBase + ".tsx", relBase + ".mts");
|
|
10977
|
-
candidates.push(
|
|
11041
|
+
candidates.push(path27.join(relBase, "index.ts"));
|
|
10978
11042
|
for (const candidate of candidates) {
|
|
10979
|
-
const absCandidate =
|
|
11043
|
+
const absCandidate = path27.join(projectRoot, candidate);
|
|
10980
11044
|
if (await fileExists(absCandidate)) {
|
|
10981
11045
|
return candidate;
|
|
10982
11046
|
}
|
|
@@ -10984,7 +11048,7 @@ async function resolveImportPath2(projectRoot, fromFile, importSource) {
|
|
|
10984
11048
|
return null;
|
|
10985
11049
|
}
|
|
10986
11050
|
async function findTestFiles(projectRoot, sourceFile) {
|
|
10987
|
-
const baseName =
|
|
11051
|
+
const baseName = path27.basename(sourceFile, path27.extname(sourceFile));
|
|
10988
11052
|
const pattern = `**/${baseName}.{test,spec}.{ts,tsx,mts}`;
|
|
10989
11053
|
const results = await findFiles(pattern, projectRoot);
|
|
10990
11054
|
return results.map((f) => relativePosix(projectRoot, f));
|
|
@@ -11752,7 +11816,7 @@ function normalizePath(filePath, projectRoot) {
|
|
|
11752
11816
|
let normalized = filePath;
|
|
11753
11817
|
normalized = normalized.replace(/\\/g, "/");
|
|
11754
11818
|
const normalizedRoot = projectRoot.replace(/\\/g, "/");
|
|
11755
|
-
if (
|
|
11819
|
+
if (path28.isAbsolute(normalized)) {
|
|
11756
11820
|
const root = normalizedRoot.endsWith("/") ? normalizedRoot : normalizedRoot + "/";
|
|
11757
11821
|
if (normalized.startsWith(root)) {
|
|
11758
11822
|
normalized = normalized.slice(root.length);
|
|
@@ -11764,12 +11828,12 @@ function normalizePath(filePath, projectRoot) {
|
|
|
11764
11828
|
return normalized;
|
|
11765
11829
|
}
|
|
11766
11830
|
function resolveImportPath22(currentFile, importPath) {
|
|
11767
|
-
const dir =
|
|
11768
|
-
let resolved =
|
|
11831
|
+
const dir = path28.dirname(currentFile);
|
|
11832
|
+
let resolved = path28.join(dir, importPath).replace(/\\/g, "/");
|
|
11769
11833
|
if (!resolved.match(/\.(ts|tsx|js|jsx)$/)) {
|
|
11770
11834
|
resolved += ".ts";
|
|
11771
11835
|
}
|
|
11772
|
-
return
|
|
11836
|
+
return path28.normalize(resolved).replace(/\\/g, "/");
|
|
11773
11837
|
}
|
|
11774
11838
|
function enqueueImports(content, current, visited, queue, maxDepth) {
|
|
11775
11839
|
const importRegex = /import\s+.*?from\s+['"]([^'"]+)['"]/g;
|
|
@@ -11801,7 +11865,7 @@ function isMechanicallyExcluded(finding, exclusionSet, projectRoot) {
|
|
|
11801
11865
|
const normalizedFile = normalizePath(finding.file, projectRoot);
|
|
11802
11866
|
if (exclusionSet.isExcluded(normalizedFile, finding.lineRange)) return true;
|
|
11803
11867
|
if (exclusionSet.isExcluded(finding.file, finding.lineRange)) return true;
|
|
11804
|
-
const absoluteFile =
|
|
11868
|
+
const absoluteFile = path28.isAbsolute(finding.file) ? finding.file : path28.join(projectRoot, finding.file).replace(/\\/g, "/");
|
|
11805
11869
|
return exclusionSet.isExcluded(absoluteFile, finding.lineRange);
|
|
11806
11870
|
}
|
|
11807
11871
|
async function validateWithGraph(crossFileRefs, graph) {
|
|
@@ -12809,7 +12873,7 @@ function isRegression(from, to) {
|
|
|
12809
12873
|
}
|
|
12810
12874
|
function collectAutopilotStatuses(autopilotPath, featurePlans, allTaskStatuses) {
|
|
12811
12875
|
try {
|
|
12812
|
-
const raw =
|
|
12876
|
+
const raw = fs29.readFileSync(autopilotPath, "utf-8");
|
|
12813
12877
|
const autopilot = JSON.parse(raw);
|
|
12814
12878
|
if (!autopilot.phases) return;
|
|
12815
12879
|
const linkedPhases = autopilot.phases.filter(
|
|
@@ -12841,10 +12905,10 @@ function inferStatus(feature, projectPath, allFeatures) {
|
|
|
12841
12905
|
const featuresWithPlans = allFeatures.filter((f) => f.plans.length > 0);
|
|
12842
12906
|
const useRootState = featuresWithPlans.length <= 1;
|
|
12843
12907
|
if (useRootState) {
|
|
12844
|
-
const rootStatePath =
|
|
12845
|
-
if (
|
|
12908
|
+
const rootStatePath = path29.join(projectPath, ".harness", "state.json");
|
|
12909
|
+
if (fs29.existsSync(rootStatePath)) {
|
|
12846
12910
|
try {
|
|
12847
|
-
const raw =
|
|
12911
|
+
const raw = fs29.readFileSync(rootStatePath, "utf-8");
|
|
12848
12912
|
const state = JSON.parse(raw);
|
|
12849
12913
|
if (state.progress) {
|
|
12850
12914
|
for (const status of Object.values(state.progress)) {
|
|
@@ -12855,14 +12919,14 @@ function inferStatus(feature, projectPath, allFeatures) {
|
|
|
12855
12919
|
}
|
|
12856
12920
|
}
|
|
12857
12921
|
}
|
|
12858
|
-
const sessionsDir =
|
|
12859
|
-
if (
|
|
12922
|
+
const sessionsDir = path29.join(projectPath, ".harness", "sessions");
|
|
12923
|
+
if (fs29.existsSync(sessionsDir)) {
|
|
12860
12924
|
try {
|
|
12861
|
-
const sessionDirs =
|
|
12925
|
+
const sessionDirs = fs29.readdirSync(sessionsDir, { withFileTypes: true });
|
|
12862
12926
|
for (const entry of sessionDirs) {
|
|
12863
12927
|
if (!entry.isDirectory()) continue;
|
|
12864
|
-
const autopilotPath =
|
|
12865
|
-
if (!
|
|
12928
|
+
const autopilotPath = path29.join(sessionsDir, entry.name, "autopilot-state.json");
|
|
12929
|
+
if (!fs29.existsSync(autopilotPath)) continue;
|
|
12866
12930
|
collectAutopilotStatuses(autopilotPath, feature.plans, allTaskStatuses);
|
|
12867
12931
|
}
|
|
12868
12932
|
} catch {
|
|
@@ -12944,7 +13008,7 @@ function parseRepoParts(repo) {
|
|
|
12944
13008
|
return { owner: parts[0], repo: parts[1] };
|
|
12945
13009
|
}
|
|
12946
13010
|
function sleep2(ms) {
|
|
12947
|
-
return new Promise((
|
|
13011
|
+
return new Promise((resolve9) => setTimeout(resolve9, ms));
|
|
12948
13012
|
}
|
|
12949
13013
|
async function fetchWithRetry(fetchFn, input, init, opts = RETRY_DEFAULTS) {
|
|
12950
13014
|
let lastResponse;
|
|
@@ -12977,9 +13041,9 @@ function isValidTrackerShape(tracker) {
|
|
|
12977
13041
|
}
|
|
12978
13042
|
function loadTrackerSyncConfig(projectRoot) {
|
|
12979
13043
|
try {
|
|
12980
|
-
const configPath =
|
|
12981
|
-
if (!
|
|
12982
|
-
const raw =
|
|
13044
|
+
const configPath = path30.join(projectRoot, "harness.config.json");
|
|
13045
|
+
if (!fs30.existsSync(configPath)) return null;
|
|
13046
|
+
const raw = fs30.readFileSync(configPath, "utf-8");
|
|
12983
13047
|
const config = JSON.parse(raw);
|
|
12984
13048
|
const tracker = config.roadmap?.tracker;
|
|
12985
13049
|
if (!isValidTrackerShape(tracker)) return null;
|
|
@@ -13094,12 +13158,12 @@ async function syncFromExternal(roadmap, adapter2, config, options, prefetchedTi
|
|
|
13094
13158
|
async function fullSync(roadmapPath, adapter2, config, options) {
|
|
13095
13159
|
const previousSync = syncMutex;
|
|
13096
13160
|
let releaseMutex;
|
|
13097
|
-
syncMutex = new Promise((
|
|
13098
|
-
releaseMutex =
|
|
13161
|
+
syncMutex = new Promise((resolve9) => {
|
|
13162
|
+
releaseMutex = resolve9;
|
|
13099
13163
|
});
|
|
13100
13164
|
await previousSync;
|
|
13101
13165
|
try {
|
|
13102
|
-
const raw =
|
|
13166
|
+
const raw = fs31.readFileSync(roadmapPath, "utf-8");
|
|
13103
13167
|
const parseResult = parseRoadmap(raw);
|
|
13104
13168
|
if (!parseResult.ok) {
|
|
13105
13169
|
return {
|
|
@@ -13112,7 +13176,7 @@ async function fullSync(roadmapPath, adapter2, config, options) {
|
|
|
13112
13176
|
const tickets = fetchResult.ok ? fetchResult.value : void 0;
|
|
13113
13177
|
const pushResult = await syncToExternal(roadmap, adapter2, config, tickets);
|
|
13114
13178
|
const pullResult = await syncFromExternal(roadmap, adapter2, config, options);
|
|
13115
|
-
|
|
13179
|
+
fs31.writeFileSync(roadmapPath, serializeRoadmap(roadmap), "utf-8");
|
|
13116
13180
|
return {
|
|
13117
13181
|
created: pushResult.created,
|
|
13118
13182
|
updated: pushResult.updated,
|
|
@@ -13123,6 +13187,32 @@ async function fullSync(roadmapPath, adapter2, config, options) {
|
|
|
13123
13187
|
releaseMutex();
|
|
13124
13188
|
}
|
|
13125
13189
|
}
|
|
13190
|
+
function scoreRoadmapCandidatesFileLess(features, _options) {
|
|
13191
|
+
const allNames = new Set(features.map((f) => f.name.toLowerCase()));
|
|
13192
|
+
const doneNames = new Set(
|
|
13193
|
+
features.filter((f) => f.status === "done").map((f) => f.name.toLowerCase())
|
|
13194
|
+
);
|
|
13195
|
+
const eligible = features.filter((f) => {
|
|
13196
|
+
if (f.status !== "planned" && f.status !== "backlog") return false;
|
|
13197
|
+
return !f.blockedBy.some((b) => {
|
|
13198
|
+
const k = b.toLowerCase();
|
|
13199
|
+
return allNames.has(k) && !doneNames.has(k);
|
|
13200
|
+
});
|
|
13201
|
+
});
|
|
13202
|
+
const candidates = eligible.map((f) => ({
|
|
13203
|
+
feature: f,
|
|
13204
|
+
priorityTier: f.priority ? PRIORITY_RANK[f.priority] : null
|
|
13205
|
+
}));
|
|
13206
|
+
candidates.sort((a, b) => {
|
|
13207
|
+
const ap = a.priorityTier;
|
|
13208
|
+
const bp = b.priorityTier;
|
|
13209
|
+
if (ap !== null && bp === null) return -1;
|
|
13210
|
+
if (ap === null && bp !== null) return 1;
|
|
13211
|
+
if (ap !== null && bp !== null && ap !== bp) return ap - bp;
|
|
13212
|
+
return a.feature.createdAt.localeCompare(b.feature.createdAt);
|
|
13213
|
+
});
|
|
13214
|
+
return candidates;
|
|
13215
|
+
}
|
|
13126
13216
|
function isEligibleCandidate(feature, allFeatureNames, doneFeatures) {
|
|
13127
13217
|
if (feature.status !== "planned" && feature.status !== "backlog") return false;
|
|
13128
13218
|
const isBlocked = feature.blockedBy.some((blocker) => {
|
|
@@ -13192,7 +13282,7 @@ function scoreRoadmapCandidates(roadmap, options) {
|
|
|
13192
13282
|
userCompletedFeatures
|
|
13193
13283
|
);
|
|
13194
13284
|
const weightedScore = POSITION_WEIGHT * positionScore + DEPENDENTS_WEIGHT * dependentsScore + AFFINITY_WEIGHT * affinityScore;
|
|
13195
|
-
const priorityTier = feature.priority ?
|
|
13285
|
+
const priorityTier = feature.priority ? PRIORITY_RANK2[feature.priority] : null;
|
|
13196
13286
|
candidates.push({
|
|
13197
13287
|
feature,
|
|
13198
13288
|
milestone: ms.name,
|
|
@@ -13232,9 +13322,550 @@ function assignFeature(roadmap, feature, assignee, date) {
|
|
|
13232
13322
|
date
|
|
13233
13323
|
});
|
|
13234
13324
|
}
|
|
13325
|
+
function scoreRoadmapCandidatesForMode(roadmap, options, config) {
|
|
13326
|
+
const mode = getRoadmapMode(config);
|
|
13327
|
+
if (mode === "file-less") {
|
|
13328
|
+
const flatFeatures = roadmap.milestones.flatMap((m) => m.features);
|
|
13329
|
+
const tracked = flatFeatures.map(featureToTrackedFeature);
|
|
13330
|
+
const fileLess = scoreRoadmapCandidatesFileLess(tracked, options);
|
|
13331
|
+
const featuresByName = new Map(flatFeatures.map((f) => [f.name, f]));
|
|
13332
|
+
return fileLess.map((c) => {
|
|
13333
|
+
const feature = featuresByName.get(c.feature.name);
|
|
13334
|
+
if (!feature) {
|
|
13335
|
+
throw new Error(`Internal: feature ${c.feature.name} lost during mode dispatch`);
|
|
13336
|
+
}
|
|
13337
|
+
return {
|
|
13338
|
+
feature,
|
|
13339
|
+
milestone: "",
|
|
13340
|
+
positionScore: 0,
|
|
13341
|
+
dependentsScore: 0,
|
|
13342
|
+
affinityScore: 0,
|
|
13343
|
+
weightedScore: 0,
|
|
13344
|
+
priorityTier: c.priorityTier
|
|
13345
|
+
};
|
|
13346
|
+
});
|
|
13347
|
+
}
|
|
13348
|
+
return scoreRoadmapCandidates(roadmap, options);
|
|
13349
|
+
}
|
|
13350
|
+
function featureToTrackedFeature(f) {
|
|
13351
|
+
return {
|
|
13352
|
+
externalId: f.externalId ?? "",
|
|
13353
|
+
name: f.name,
|
|
13354
|
+
status: f.status,
|
|
13355
|
+
summary: f.summary,
|
|
13356
|
+
spec: f.spec,
|
|
13357
|
+
plans: f.plans,
|
|
13358
|
+
blockedBy: f.blockedBy,
|
|
13359
|
+
assignee: f.assignee,
|
|
13360
|
+
priority: f.priority,
|
|
13361
|
+
milestone: null,
|
|
13362
|
+
createdAt: "1970-01-01T00:00:00Z",
|
|
13363
|
+
updatedAt: f.updatedAt
|
|
13364
|
+
};
|
|
13365
|
+
}
|
|
13366
|
+
function parseExternalId2(externalId) {
|
|
13367
|
+
const m = externalId.match(/^github:([^/]+)\/([^#]+)#(\d+)$/);
|
|
13368
|
+
if (!m) return null;
|
|
13369
|
+
return { owner: m[1], repo: m[2], number: parseInt(m[3], 10) };
|
|
13370
|
+
}
|
|
13371
|
+
function buildExternalId2(owner, repo, n) {
|
|
13372
|
+
return `github:${owner}/${repo}#${n}`;
|
|
13373
|
+
}
|
|
13374
|
+
function parseBodyBlock(body) {
|
|
13375
|
+
const startIdx = body.indexOf(START_MARKER);
|
|
13376
|
+
if (startIdx === -1) return { summary: body.trim(), meta: {} };
|
|
13377
|
+
const endIdx = body.indexOf(END_MARKER, startIdx + START_MARKER.length);
|
|
13378
|
+
if (endIdx === -1) return { summary: body.trim(), meta: {} };
|
|
13379
|
+
const secondStart = body.indexOf(START_MARKER, endIdx + END_MARKER.length);
|
|
13380
|
+
if (secondStart !== -1) {
|
|
13381
|
+
console.warn("harness-meta: multiple blocks found; first wins.");
|
|
13382
|
+
}
|
|
13383
|
+
const yamlText = body.slice(startIdx + START_MARKER.length, endIdx).trim();
|
|
13384
|
+
let meta = {};
|
|
13385
|
+
try {
|
|
13386
|
+
const parsed = parseYaml(yamlText);
|
|
13387
|
+
if (parsed && typeof parsed === "object") {
|
|
13388
|
+
meta = normalizeBodyMeta(parsed);
|
|
13389
|
+
}
|
|
13390
|
+
} catch (e) {
|
|
13391
|
+
console.warn(
|
|
13392
|
+
`harness-meta: malformed YAML, treating block as missing: ${e.message}`
|
|
13393
|
+
);
|
|
13394
|
+
meta = {};
|
|
13395
|
+
}
|
|
13396
|
+
const summary = body.slice(0, startIdx).trim();
|
|
13397
|
+
return { summary, meta };
|
|
13398
|
+
}
|
|
13399
|
+
function normalizeBodyMeta(raw) {
|
|
13400
|
+
const out = {};
|
|
13401
|
+
if (typeof raw.spec === "string") out.spec = raw.spec;
|
|
13402
|
+
if (typeof raw.plan === "string") out.plan = raw.plan;
|
|
13403
|
+
if (typeof raw.blocked_by === "string") {
|
|
13404
|
+
out.blocked_by = raw.blocked_by.split(",").map((s) => s.trim()).filter(Boolean);
|
|
13405
|
+
} else if (Array.isArray(raw.blocked_by)) {
|
|
13406
|
+
out.blocked_by = raw.blocked_by.filter(
|
|
13407
|
+
(v) => typeof v === "string"
|
|
13408
|
+
);
|
|
13409
|
+
}
|
|
13410
|
+
if (typeof raw.priority === "string" && ["P0", "P1", "P2", "P3"].includes(raw.priority)) {
|
|
13411
|
+
out.priority = raw.priority;
|
|
13412
|
+
}
|
|
13413
|
+
if (typeof raw.milestone === "string") out.milestone = raw.milestone;
|
|
13414
|
+
return out;
|
|
13415
|
+
}
|
|
13416
|
+
function serializeBodyBlock(summary, meta) {
|
|
13417
|
+
const ordered = {};
|
|
13418
|
+
for (const [key, yamlKey, mapValue] of SERIALIZER_FIELDS) {
|
|
13419
|
+
const raw = meta[key];
|
|
13420
|
+
if (raw === void 0 || raw === null) continue;
|
|
13421
|
+
const out = mapValue(raw);
|
|
13422
|
+
if (out === void 0) continue;
|
|
13423
|
+
ordered[yamlKey] = out;
|
|
13424
|
+
}
|
|
13425
|
+
const trimmed = summary.trim();
|
|
13426
|
+
if (Object.keys(ordered).length === 0) return trimmed;
|
|
13427
|
+
const yamlBody = stringifyYaml(ordered).trimEnd();
|
|
13428
|
+
const block = `${START_MARKER}
|
|
13429
|
+
${yamlBody}
|
|
13430
|
+
${END_MARKER}`;
|
|
13431
|
+
return trimmed.length > 0 ? `${trimmed}
|
|
13432
|
+
|
|
13433
|
+
${block}` : block;
|
|
13434
|
+
}
|
|
13435
|
+
function compareStatus(server, patch) {
|
|
13436
|
+
if (patch.status === void 0) return { idempotent: true };
|
|
13437
|
+
if (server.status === "done" && patch.status !== "done") {
|
|
13438
|
+
return { diff: { ours: patch.status, theirs: server.status }, idempotent: false };
|
|
13439
|
+
}
|
|
13440
|
+
return { idempotent: server.status === patch.status };
|
|
13441
|
+
}
|
|
13442
|
+
function compareAssignee(server, patch) {
|
|
13443
|
+
if (patch.assignee === void 0) return { idempotent: true };
|
|
13444
|
+
if (server.assignee !== null && server.assignee !== patch.assignee) {
|
|
13445
|
+
return { diff: { ours: patch.assignee, theirs: server.assignee }, idempotent: false };
|
|
13446
|
+
}
|
|
13447
|
+
return { idempotent: server.assignee === patch.assignee };
|
|
13448
|
+
}
|
|
13449
|
+
function compareScalar(field, server, patch) {
|
|
13450
|
+
const next = patch[field];
|
|
13451
|
+
if (next === void 0) return { idempotent: true };
|
|
13452
|
+
const cur = server[field];
|
|
13453
|
+
if (cur !== null && cur !== next) {
|
|
13454
|
+
return { diff: { ours: next, theirs: cur }, idempotent: false };
|
|
13455
|
+
}
|
|
13456
|
+
return { idempotent: cur === next };
|
|
13457
|
+
}
|
|
13458
|
+
function compareArray(field, server, patch) {
|
|
13459
|
+
const next = patch[field];
|
|
13460
|
+
if (next === void 0) return { idempotent: true };
|
|
13461
|
+
const cur = server[field];
|
|
13462
|
+
if (arraysEqual2(next, cur)) return { idempotent: true };
|
|
13463
|
+
if (cur.length > 0 && cur.some((v) => !next.includes(v))) {
|
|
13464
|
+
return { diff: { ours: next, theirs: cur }, idempotent: false };
|
|
13465
|
+
}
|
|
13466
|
+
return { idempotent: false };
|
|
13467
|
+
}
|
|
13468
|
+
function refetchAndCompare(server, patch) {
|
|
13469
|
+
const verdicts = [
|
|
13470
|
+
["status", compareStatus(server, patch)],
|
|
13471
|
+
["assignee", compareAssignee(server, patch)],
|
|
13472
|
+
["priority", compareScalar("priority", server, patch)],
|
|
13473
|
+
["milestone", compareScalar("milestone", server, patch)],
|
|
13474
|
+
["spec", compareScalar("spec", server, patch)],
|
|
13475
|
+
["plans", compareArray("plans", server, patch)],
|
|
13476
|
+
["blockedBy", compareArray("blockedBy", server, patch)]
|
|
13477
|
+
];
|
|
13478
|
+
const diff2 = {};
|
|
13479
|
+
let idempotent = true;
|
|
13480
|
+
for (const [field, verdict] of verdicts) {
|
|
13481
|
+
if (verdict.diff) diff2[field] = verdict.diff;
|
|
13482
|
+
if (!verdict.idempotent) idempotent = false;
|
|
13483
|
+
}
|
|
13484
|
+
if (Object.keys(diff2).length > 0) return { ok: false, diff: diff2 };
|
|
13485
|
+
return { ok: true, idempotent };
|
|
13486
|
+
}
|
|
13487
|
+
function arraysEqual2(a, b) {
|
|
13488
|
+
if (a.length !== b.length) return false;
|
|
13489
|
+
const sa = [...a].sort();
|
|
13490
|
+
const sb = [...b].sort();
|
|
13491
|
+
return sa.every((v, i) => v === sb[i]);
|
|
13492
|
+
}
|
|
13493
|
+
function metaFromFeatureFields(src) {
|
|
13494
|
+
const meta = {};
|
|
13495
|
+
if (src.spec !== void 0 && src.spec !== null) meta.spec = src.spec;
|
|
13496
|
+
if (src.plans && src.plans.length > 0) meta.plan = src.plans[0];
|
|
13497
|
+
if (src.blockedBy && src.blockedBy.length > 0) meta.blocked_by = src.blockedBy;
|
|
13498
|
+
if (src.priority !== void 0 && src.priority !== null) meta.priority = src.priority;
|
|
13499
|
+
if (src.milestone !== void 0 && src.milestone !== null) meta.milestone = src.milestone;
|
|
13500
|
+
return meta;
|
|
13501
|
+
}
|
|
13502
|
+
function buildCreateMeta(feature) {
|
|
13503
|
+
return metaFromFeatureFields(feature);
|
|
13504
|
+
}
|
|
13505
|
+
function createTrackerClient(config) {
|
|
13506
|
+
if (config.kind !== "github-issues") {
|
|
13507
|
+
return Err(new Error(`Unsupported tracker kind: "${String(config.kind)}"`));
|
|
13508
|
+
}
|
|
13509
|
+
const token = config.token ?? process.env.GITHUB_TOKEN;
|
|
13510
|
+
if (!token) {
|
|
13511
|
+
return Err(
|
|
13512
|
+
new Error("createTrackerClient: missing GitHub token (config.token or GITHUB_TOKEN env)")
|
|
13513
|
+
);
|
|
13514
|
+
}
|
|
13515
|
+
const opts = { token, repo: config.repo };
|
|
13516
|
+
if (config.apiBase !== void 0) opts.apiBase = config.apiBase;
|
|
13517
|
+
if (config.selectorLabel !== void 0) opts.selectorLabel = config.selectorLabel;
|
|
13518
|
+
if (config.etagStore !== void 0) opts.etagStore = config.etagStore;
|
|
13519
|
+
return Ok(new GitHubIssuesTrackerAdapter(opts));
|
|
13520
|
+
}
|
|
13521
|
+
function makeTrackerConflictBody(err, opts = {}) {
|
|
13522
|
+
return {
|
|
13523
|
+
error: err.message,
|
|
13524
|
+
code: "TRACKER_CONFLICT",
|
|
13525
|
+
externalId: err.externalId,
|
|
13526
|
+
conflictedWith: opts.conflictedWith ?? err.diff,
|
|
13527
|
+
refreshHint: "reload-roadmap"
|
|
13528
|
+
};
|
|
13529
|
+
}
|
|
13530
|
+
function loadProjectRoadmapMode(projectRoot) {
|
|
13531
|
+
try {
|
|
13532
|
+
const configPath = path31.join(projectRoot, "harness.config.json");
|
|
13533
|
+
if (!fs32.existsSync(configPath)) return getRoadmapMode(null);
|
|
13534
|
+
const raw = fs32.readFileSync(configPath, "utf-8");
|
|
13535
|
+
const parsed = JSON.parse(raw);
|
|
13536
|
+
return getRoadmapMode(parsed);
|
|
13537
|
+
} catch {
|
|
13538
|
+
return getRoadmapMode(null);
|
|
13539
|
+
}
|
|
13540
|
+
}
|
|
13541
|
+
function loadTrackerClientConfigFromProject(projectRoot) {
|
|
13542
|
+
try {
|
|
13543
|
+
const configPath = path32.join(projectRoot, "harness.config.json");
|
|
13544
|
+
if (!fs33.existsSync(configPath)) {
|
|
13545
|
+
return Err(new Error("harness.config.json not found"));
|
|
13546
|
+
}
|
|
13547
|
+
const cfg = JSON.parse(fs33.readFileSync(configPath, "utf-8"));
|
|
13548
|
+
const tracker = cfg.roadmap?.tracker;
|
|
13549
|
+
if (!tracker) {
|
|
13550
|
+
return Err(
|
|
13551
|
+
new Error(
|
|
13552
|
+
"file-less tracker config missing: set roadmap.tracker.kind in harness.config.json"
|
|
13553
|
+
)
|
|
13554
|
+
);
|
|
13555
|
+
}
|
|
13556
|
+
if (tracker.kind !== "github") {
|
|
13557
|
+
return Err(
|
|
13558
|
+
new Error(`file-less tracker only supports kind: "github" today; got "${tracker.kind}"`)
|
|
13559
|
+
);
|
|
13560
|
+
}
|
|
13561
|
+
if (!tracker.repo) {
|
|
13562
|
+
return Err(
|
|
13563
|
+
new Error(
|
|
13564
|
+
"roadmap.tracker.repo is required for file-less mode (set it in harness.config.json)"
|
|
13565
|
+
)
|
|
13566
|
+
);
|
|
13567
|
+
}
|
|
13568
|
+
return Ok({ kind: "github-issues", repo: tracker.repo });
|
|
13569
|
+
} catch (e) {
|
|
13570
|
+
return Err(e instanceof Error ? e : new Error(String(e)));
|
|
13571
|
+
}
|
|
13572
|
+
}
|
|
13573
|
+
function bodyMetaMatches(a, b) {
|
|
13574
|
+
return eqOpt(a.spec, b.spec) && eqOpt(a.plan, b.plan) && eqOpt(a.priority, b.priority) && eqOpt(a.milestone, b.milestone) && eqList(a.blocked_by, b.blocked_by);
|
|
13575
|
+
}
|
|
13576
|
+
function eqOpt(x, y) {
|
|
13577
|
+
const xx = x ?? null;
|
|
13578
|
+
const yy = y ?? null;
|
|
13579
|
+
return xx === yy;
|
|
13580
|
+
}
|
|
13581
|
+
function eqList(x, y) {
|
|
13582
|
+
const xx = (x ?? []).slice().sort();
|
|
13583
|
+
const yy = (y ?? []).slice().sort();
|
|
13584
|
+
if (xx.length !== yy.length) return false;
|
|
13585
|
+
for (let i = 0; i < xx.length; i++) if (xx[i] !== yy[i]) return false;
|
|
13586
|
+
return true;
|
|
13587
|
+
}
|
|
13588
|
+
function normalizeAt(at) {
|
|
13589
|
+
if (/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z$/.test(at)) return at;
|
|
13590
|
+
const d = new Date(at);
|
|
13591
|
+
if (Number.isNaN(d.getTime())) return at;
|
|
13592
|
+
const seconds = Math.floor(d.getTime() / 1e3) * 1e3;
|
|
13593
|
+
return new Date(seconds).toISOString().replace(/\.\d{3}Z$/, "Z");
|
|
13594
|
+
}
|
|
13595
|
+
function hashHistoryEvent(event) {
|
|
13596
|
+
const detailsStr = JSON.stringify(event.details ?? {});
|
|
13597
|
+
const at = normalizeAt(event.at);
|
|
13598
|
+
const input = `${event.type}|${event.actor}|${at}|${detailsStr}`;
|
|
13599
|
+
return createHash4("sha256").update(input).digest("hex").slice(0, 8);
|
|
13600
|
+
}
|
|
13601
|
+
function parseHashFromCommentBody(commentBody) {
|
|
13602
|
+
const match = commentBody.match(HASH_RE);
|
|
13603
|
+
return match ? match[1] : null;
|
|
13604
|
+
}
|
|
13605
|
+
function buildHistoryCommentBody(event) {
|
|
13606
|
+
const hash = hashHistoryEvent(event);
|
|
13607
|
+
return `<!-- harness-history hash:${hash} -->
|
|
13608
|
+
${JSON.stringify(event)}`;
|
|
13609
|
+
}
|
|
13610
|
+
async function buildMigrationPlan(roadmap, existingFeatures, fetchHistoryHashes, getRawBodyForExternalId) {
|
|
13611
|
+
const plan = {
|
|
13612
|
+
toCreate: [],
|
|
13613
|
+
toUpdate: [],
|
|
13614
|
+
unchanged: [],
|
|
13615
|
+
historyToAppend: [],
|
|
13616
|
+
ambiguous: []
|
|
13617
|
+
};
|
|
13618
|
+
const byExternalId = /* @__PURE__ */ new Map();
|
|
13619
|
+
const byNameLower = /* @__PURE__ */ new Map();
|
|
13620
|
+
for (const t of existingFeatures) {
|
|
13621
|
+
byExternalId.set(t.externalId, t);
|
|
13622
|
+
byNameLower.set(t.name.toLowerCase(), t);
|
|
13623
|
+
}
|
|
13624
|
+
const featureToExternalId = /* @__PURE__ */ new Map();
|
|
13625
|
+
for (const ms of roadmap.milestones) {
|
|
13626
|
+
for (const f of ms.features) {
|
|
13627
|
+
if (f.externalId) featureToExternalId.set(f.name, f.externalId);
|
|
13628
|
+
}
|
|
13629
|
+
}
|
|
13630
|
+
for (const ms of roadmap.milestones) {
|
|
13631
|
+
for (const feature of ms.features) {
|
|
13632
|
+
if (feature.externalId == null) {
|
|
13633
|
+
const existing = byNameLower.get(feature.name.toLowerCase());
|
|
13634
|
+
if (existing) {
|
|
13635
|
+
plan.ambiguous.push({ name: feature.name, existingIssueRef: existing.externalId });
|
|
13636
|
+
} else {
|
|
13637
|
+
plan.toCreate.push({
|
|
13638
|
+
name: feature.name,
|
|
13639
|
+
input: featureToNewInput(feature, ms)
|
|
13640
|
+
});
|
|
13641
|
+
}
|
|
13642
|
+
continue;
|
|
13643
|
+
}
|
|
13644
|
+
const tracked = byExternalId.get(feature.externalId);
|
|
13645
|
+
if (!tracked) {
|
|
13646
|
+
plan.ambiguous.push({
|
|
13647
|
+
name: feature.name,
|
|
13648
|
+
existingIssueRef: `${feature.externalId} (recorded external id not found)`
|
|
13649
|
+
});
|
|
13650
|
+
continue;
|
|
13651
|
+
}
|
|
13652
|
+
const expectedMeta = featureToExpectedMeta(feature, ms);
|
|
13653
|
+
const rawBody = await getRawBodyForExternalId(feature.externalId);
|
|
13654
|
+
const actualMeta = rawBody == null ? {} : parseBodyBlock(rawBody).meta;
|
|
13655
|
+
if (rawBody != null && bodyMetaMatches(actualMeta, expectedMeta)) {
|
|
13656
|
+
plan.unchanged.push({ externalId: feature.externalId, name: feature.name });
|
|
13657
|
+
} else {
|
|
13658
|
+
const diff2 = formatDiff(actualMeta, expectedMeta);
|
|
13659
|
+
const patch = metaToPatch(feature, expectedMeta);
|
|
13660
|
+
plan.toUpdate.push({
|
|
13661
|
+
externalId: feature.externalId,
|
|
13662
|
+
name: feature.name,
|
|
13663
|
+
patch,
|
|
13664
|
+
diff: diff2
|
|
13665
|
+
});
|
|
13666
|
+
}
|
|
13667
|
+
}
|
|
13668
|
+
}
|
|
13669
|
+
for (const record of roadmap.assignmentHistory) {
|
|
13670
|
+
const externalId = featureToExternalId.get(record.feature);
|
|
13671
|
+
if (!externalId) {
|
|
13672
|
+
console.warn(
|
|
13673
|
+
`harness-migrate: cannot resolve external id for assignment-history feature "${record.feature}"; skipping.`
|
|
13674
|
+
);
|
|
13675
|
+
continue;
|
|
13676
|
+
}
|
|
13677
|
+
const type = mapAction(record.action);
|
|
13678
|
+
const event = {
|
|
13679
|
+
type,
|
|
13680
|
+
actor: record.assignee,
|
|
13681
|
+
at: record.date
|
|
13682
|
+
};
|
|
13683
|
+
const hash = hashHistoryEvent(event);
|
|
13684
|
+
const existingHashes = await fetchHistoryHashes(externalId);
|
|
13685
|
+
if (existingHashes.has(hash)) continue;
|
|
13686
|
+
plan.historyToAppend.push({ externalId, event, hash });
|
|
13687
|
+
}
|
|
13688
|
+
return plan;
|
|
13689
|
+
}
|
|
13690
|
+
function featureToExpectedMeta(feature, milestone) {
|
|
13691
|
+
const meta = {};
|
|
13692
|
+
if (feature.spec != null) meta.spec = feature.spec;
|
|
13693
|
+
if (feature.plans.length > 0) meta.plan = feature.plans[0] ?? null;
|
|
13694
|
+
if (feature.blockedBy.length > 0) meta.blocked_by = feature.blockedBy.slice().sort();
|
|
13695
|
+
if (feature.priority != null) meta.priority = feature.priority;
|
|
13696
|
+
if (!milestone.isBacklog) meta.milestone = milestone.name;
|
|
13697
|
+
return meta;
|
|
13698
|
+
}
|
|
13699
|
+
function featureToNewInput(feature, milestone) {
|
|
13700
|
+
const input = {
|
|
13701
|
+
name: feature.name,
|
|
13702
|
+
summary: feature.summary,
|
|
13703
|
+
status: feature.status,
|
|
13704
|
+
spec: feature.spec,
|
|
13705
|
+
plans: feature.plans,
|
|
13706
|
+
blockedBy: feature.blockedBy,
|
|
13707
|
+
priority: feature.priority,
|
|
13708
|
+
milestone: milestone.isBacklog ? null : milestone.name,
|
|
13709
|
+
assignee: feature.assignee
|
|
13710
|
+
};
|
|
13711
|
+
return input;
|
|
13712
|
+
}
|
|
13713
|
+
function metaToPatch(feature, expected) {
|
|
13714
|
+
const patch = {};
|
|
13715
|
+
patch.summary = feature.summary;
|
|
13716
|
+
if (expected.spec !== void 0) patch.spec = expected.spec ?? null;
|
|
13717
|
+
if (expected.plan !== void 0 && expected.plan != null) patch.plans = [expected.plan];
|
|
13718
|
+
if (expected.blocked_by !== void 0) patch.blockedBy = expected.blocked_by ?? [];
|
|
13719
|
+
if (expected.priority !== void 0) patch.priority = expected.priority ?? null;
|
|
13720
|
+
if (expected.milestone !== void 0) patch.milestone = expected.milestone ?? null;
|
|
13721
|
+
return patch;
|
|
13722
|
+
}
|
|
13723
|
+
function formatDiff(actual, expected) {
|
|
13724
|
+
const keys = /* @__PURE__ */ new Set();
|
|
13725
|
+
const collect = (m) => {
|
|
13726
|
+
for (const k of Object.keys(m)) {
|
|
13727
|
+
const v = m[k];
|
|
13728
|
+
if (v !== void 0 && v !== null && !(Array.isArray(v) && v.length === 0)) keys.add(k);
|
|
13729
|
+
}
|
|
13730
|
+
};
|
|
13731
|
+
collect(actual);
|
|
13732
|
+
collect(expected);
|
|
13733
|
+
const changed = [];
|
|
13734
|
+
for (const key of [...keys].sort()) {
|
|
13735
|
+
const a = actual[key];
|
|
13736
|
+
const e = expected[key];
|
|
13737
|
+
if (JSON.stringify(a ?? null) !== JSON.stringify(e ?? null)) changed.push(key);
|
|
13738
|
+
}
|
|
13739
|
+
return changed.join(",");
|
|
13740
|
+
}
|
|
13741
|
+
function mapAction(action) {
|
|
13742
|
+
switch (action) {
|
|
13743
|
+
case "assigned":
|
|
13744
|
+
return "claimed";
|
|
13745
|
+
case "completed":
|
|
13746
|
+
return "completed";
|
|
13747
|
+
case "unassigned":
|
|
13748
|
+
return "released";
|
|
13749
|
+
}
|
|
13750
|
+
}
|
|
13751
|
+
async function runMigrationPlan(plan, deps, opts) {
|
|
13752
|
+
const projectRoot = opts.projectRoot;
|
|
13753
|
+
const roadmapPath = path33.join(projectRoot, "docs", "roadmap.md");
|
|
13754
|
+
const archivedPath = path33.join(projectRoot, "docs", "roadmap.md.archived");
|
|
13755
|
+
const configPath = path33.join(projectRoot, "harness.config.json");
|
|
13756
|
+
const configBackupPath = path33.join(projectRoot, "harness.config.json.pre-migration");
|
|
13757
|
+
const createdSoFar = [];
|
|
13758
|
+
const report = {
|
|
13759
|
+
created: 0,
|
|
13760
|
+
updated: 0,
|
|
13761
|
+
unchanged: plan.unchanged.length,
|
|
13762
|
+
historyAppended: 0,
|
|
13763
|
+
archivedFrom: null,
|
|
13764
|
+
archivedTo: null,
|
|
13765
|
+
configBackup: null,
|
|
13766
|
+
mode: "applied"
|
|
13767
|
+
};
|
|
13768
|
+
if (plan.ambiguous.length > 0) {
|
|
13769
|
+
const names = plan.ambiguous.map((a) => a.name).join(", ");
|
|
13770
|
+
return Ok({
|
|
13771
|
+
...report,
|
|
13772
|
+
mode: "aborted",
|
|
13773
|
+
abortReason: `ambiguous features (title-collision or dangling external-id): ${names}`
|
|
13774
|
+
});
|
|
13775
|
+
}
|
|
13776
|
+
if (opts.dryRun) {
|
|
13777
|
+
return Ok({ ...report, mode: "dry-run" });
|
|
13778
|
+
}
|
|
13779
|
+
for (const entry of plan.toCreate) {
|
|
13780
|
+
const r = await deps.client.create(entry.input);
|
|
13781
|
+
if (!r.ok) {
|
|
13782
|
+
return Ok({
|
|
13783
|
+
...report,
|
|
13784
|
+
created: createdSoFar.length,
|
|
13785
|
+
createdSoFar,
|
|
13786
|
+
mode: "aborted",
|
|
13787
|
+
abortReason: `create failed for "${entry.name}": ${r.error.message}`
|
|
13788
|
+
});
|
|
13789
|
+
}
|
|
13790
|
+
createdSoFar.push({ name: entry.name, externalId: r.value.externalId });
|
|
13791
|
+
}
|
|
13792
|
+
report.created = createdSoFar.length;
|
|
13793
|
+
for (const entry of plan.toUpdate) {
|
|
13794
|
+
const r = await deps.client.update(entry.externalId, entry.patch);
|
|
13795
|
+
if (!r.ok) {
|
|
13796
|
+
return Ok({
|
|
13797
|
+
...report,
|
|
13798
|
+
createdSoFar,
|
|
13799
|
+
mode: "aborted",
|
|
13800
|
+
abortReason: `update failed for "${entry.name}" (${entry.externalId}): ${r.error.message}`
|
|
13801
|
+
});
|
|
13802
|
+
}
|
|
13803
|
+
report.updated++;
|
|
13804
|
+
}
|
|
13805
|
+
for (const entry of plan.historyToAppend) {
|
|
13806
|
+
const r = await deps.client.appendHistory(entry.externalId, entry.event);
|
|
13807
|
+
if (!r.ok) {
|
|
13808
|
+
return Ok({
|
|
13809
|
+
...report,
|
|
13810
|
+
createdSoFar,
|
|
13811
|
+
mode: "aborted",
|
|
13812
|
+
abortReason: `appendHistory failed for ${entry.externalId} (${entry.event.type}): ${r.error.message}`
|
|
13813
|
+
});
|
|
13814
|
+
}
|
|
13815
|
+
report.historyAppended++;
|
|
13816
|
+
}
|
|
13817
|
+
if (deps.existsFile(archivedPath)) {
|
|
13818
|
+
return Ok({
|
|
13819
|
+
...report,
|
|
13820
|
+
createdSoFar,
|
|
13821
|
+
mode: "aborted",
|
|
13822
|
+
abortReason: `archive-collision: ${archivedPath} already exists; refusing to overwrite`
|
|
13823
|
+
});
|
|
13824
|
+
}
|
|
13825
|
+
if (deps.existsFile(roadmapPath)) {
|
|
13826
|
+
try {
|
|
13827
|
+
deps.renameFile(roadmapPath, archivedPath);
|
|
13828
|
+
report.archivedFrom = roadmapPath;
|
|
13829
|
+
report.archivedTo = archivedPath;
|
|
13830
|
+
} catch (err) {
|
|
13831
|
+
return Ok({
|
|
13832
|
+
...report,
|
|
13833
|
+
createdSoFar,
|
|
13834
|
+
mode: "aborted",
|
|
13835
|
+
abortReason: `archive failed: ${err.message}`
|
|
13836
|
+
});
|
|
13837
|
+
}
|
|
13838
|
+
}
|
|
13839
|
+
try {
|
|
13840
|
+
if (!deps.existsFile(configPath)) {
|
|
13841
|
+
return Ok({
|
|
13842
|
+
...report,
|
|
13843
|
+
createdSoFar,
|
|
13844
|
+
mode: "aborted",
|
|
13845
|
+
abortReason: `config rewrite failed: ${configPath} not found`
|
|
13846
|
+
});
|
|
13847
|
+
}
|
|
13848
|
+
const original = deps.readFile(configPath);
|
|
13849
|
+
deps.writeFile(configBackupPath, original);
|
|
13850
|
+
report.configBackup = configBackupPath;
|
|
13851
|
+
const parsed = JSON.parse(original);
|
|
13852
|
+
const roadmapSection = parsed.roadmap ?? {};
|
|
13853
|
+
roadmapSection.mode = "file-less";
|
|
13854
|
+
parsed.roadmap = roadmapSection;
|
|
13855
|
+
deps.writeFile(configPath, JSON.stringify(parsed, null, 2) + "\n");
|
|
13856
|
+
} catch (err) {
|
|
13857
|
+
return Ok({
|
|
13858
|
+
...report,
|
|
13859
|
+
createdSoFar,
|
|
13860
|
+
mode: "aborted",
|
|
13861
|
+
abortReason: `config rewrite failed: ${err.message}`
|
|
13862
|
+
});
|
|
13863
|
+
}
|
|
13864
|
+
return Ok(report);
|
|
13865
|
+
}
|
|
13235
13866
|
function getStatePath() {
|
|
13236
13867
|
const home = process.env["HOME"] || os.homedir();
|
|
13237
|
-
return
|
|
13868
|
+
return path36.join(home, ".harness", "update-check.json");
|
|
13238
13869
|
}
|
|
13239
13870
|
function isUpdateCheckEnabled(configInterval) {
|
|
13240
13871
|
if (process.env["HARNESS_NO_UPDATE_CHECK"] === "1") return false;
|
|
@@ -13247,7 +13878,7 @@ function shouldRunCheck(state, intervalMs) {
|
|
|
13247
13878
|
}
|
|
13248
13879
|
function readCheckState() {
|
|
13249
13880
|
try {
|
|
13250
|
-
const raw =
|
|
13881
|
+
const raw = fs36.readFileSync(getStatePath(), "utf-8");
|
|
13251
13882
|
const parsed = JSON.parse(raw);
|
|
13252
13883
|
if (typeof parsed === "object" && parsed !== null && "lastCheckTime" in parsed && typeof parsed.lastCheckTime === "number" && "currentVersion" in parsed && typeof parsed.currentVersion === "string") {
|
|
13253
13884
|
const state = parsed;
|
|
@@ -13264,7 +13895,7 @@ function readCheckState() {
|
|
|
13264
13895
|
}
|
|
13265
13896
|
function spawnBackgroundCheck(currentVersion) {
|
|
13266
13897
|
const statePath = getStatePath();
|
|
13267
|
-
const stateDir =
|
|
13898
|
+
const stateDir = path36.dirname(statePath);
|
|
13268
13899
|
const script = `
|
|
13269
13900
|
const { execSync } = require('child_process');
|
|
13270
13901
|
const fs = require('fs');
|
|
@@ -13507,14 +14138,14 @@ function getModelPrice(model, dataset) {
|
|
|
13507
14138
|
return pricing;
|
|
13508
14139
|
}
|
|
13509
14140
|
function getCachePath(projectRoot) {
|
|
13510
|
-
return
|
|
14141
|
+
return path37.join(projectRoot, ".harness", "cache", "pricing.json");
|
|
13511
14142
|
}
|
|
13512
14143
|
function getStalenessMarkerPath(projectRoot) {
|
|
13513
|
-
return
|
|
14144
|
+
return path37.join(projectRoot, ".harness", "cache", "staleness-marker.json");
|
|
13514
14145
|
}
|
|
13515
14146
|
async function readDiskCache(projectRoot) {
|
|
13516
14147
|
try {
|
|
13517
|
-
const raw = await
|
|
14148
|
+
const raw = await fs37.readFile(getCachePath(projectRoot), "utf-8");
|
|
13518
14149
|
return JSON.parse(raw);
|
|
13519
14150
|
} catch {
|
|
13520
14151
|
return null;
|
|
@@ -13522,8 +14153,8 @@ async function readDiskCache(projectRoot) {
|
|
|
13522
14153
|
}
|
|
13523
14154
|
async function writeDiskCache(projectRoot, data) {
|
|
13524
14155
|
const cachePath = getCachePath(projectRoot);
|
|
13525
|
-
await
|
|
13526
|
-
await
|
|
14156
|
+
await fs37.mkdir(path37.dirname(cachePath), { recursive: true });
|
|
14157
|
+
await fs37.writeFile(cachePath, JSON.stringify(data, null, 2));
|
|
13527
14158
|
}
|
|
13528
14159
|
async function fetchFromNetwork() {
|
|
13529
14160
|
try {
|
|
@@ -13550,7 +14181,7 @@ function loadFallbackDataset() {
|
|
|
13550
14181
|
async function checkAndWarnStaleness(projectRoot) {
|
|
13551
14182
|
const markerPath = getStalenessMarkerPath(projectRoot);
|
|
13552
14183
|
try {
|
|
13553
|
-
const raw = await
|
|
14184
|
+
const raw = await fs37.readFile(markerPath, "utf-8");
|
|
13554
14185
|
const marker = JSON.parse(raw);
|
|
13555
14186
|
const firstUse = new Date(marker.firstFallbackUse).getTime();
|
|
13556
14187
|
const now = Date.now();
|
|
@@ -13562,8 +14193,8 @@ async function checkAndWarnStaleness(projectRoot) {
|
|
|
13562
14193
|
}
|
|
13563
14194
|
} catch {
|
|
13564
14195
|
try {
|
|
13565
|
-
await
|
|
13566
|
-
await
|
|
14196
|
+
await fs37.mkdir(path37.dirname(markerPath), { recursive: true });
|
|
14197
|
+
await fs37.writeFile(
|
|
13567
14198
|
markerPath,
|
|
13568
14199
|
JSON.stringify({ firstFallbackUse: (/* @__PURE__ */ new Date()).toISOString() })
|
|
13569
14200
|
);
|
|
@@ -13573,7 +14204,7 @@ async function checkAndWarnStaleness(projectRoot) {
|
|
|
13573
14204
|
}
|
|
13574
14205
|
async function clearStalenessMarker(projectRoot) {
|
|
13575
14206
|
try {
|
|
13576
|
-
await
|
|
14207
|
+
await fs37.unlink(getStalenessMarkerPath(projectRoot));
|
|
13577
14208
|
} catch {
|
|
13578
14209
|
}
|
|
13579
14210
|
}
|
|
@@ -13810,10 +14441,10 @@ function parseLine(line, lineNumber) {
|
|
|
13810
14441
|
return record;
|
|
13811
14442
|
}
|
|
13812
14443
|
function readCostRecords(projectRoot) {
|
|
13813
|
-
const costsFile =
|
|
14444
|
+
const costsFile = path38.join(projectRoot, ".harness", "metrics", "costs.jsonl");
|
|
13814
14445
|
let raw;
|
|
13815
14446
|
try {
|
|
13816
|
-
raw =
|
|
14447
|
+
raw = fs38.readFileSync(costsFile, "utf-8");
|
|
13817
14448
|
} catch {
|
|
13818
14449
|
return [];
|
|
13819
14450
|
}
|
|
@@ -13863,7 +14494,7 @@ function parseCCLine(line, filePath, lineNumber) {
|
|
|
13863
14494
|
entry = JSON.parse(line);
|
|
13864
14495
|
} catch {
|
|
13865
14496
|
console.warn(
|
|
13866
|
-
`[harness usage] Skipping malformed CC JSONL line ${lineNumber} in ${
|
|
14497
|
+
`[harness usage] Skipping malformed CC JSONL line ${lineNumber} in ${path39.basename(filePath)}`
|
|
13867
14498
|
);
|
|
13868
14499
|
return null;
|
|
13869
14500
|
}
|
|
@@ -13877,7 +14508,7 @@ function parseCCLine(line, filePath, lineNumber) {
|
|
|
13877
14508
|
function readCCFile(filePath) {
|
|
13878
14509
|
let raw;
|
|
13879
14510
|
try {
|
|
13880
|
-
raw =
|
|
14511
|
+
raw = fs39.readFileSync(filePath, "utf-8");
|
|
13881
14512
|
} catch {
|
|
13882
14513
|
return [];
|
|
13883
14514
|
}
|
|
@@ -13899,10 +14530,10 @@ function readCCFile(filePath) {
|
|
|
13899
14530
|
}
|
|
13900
14531
|
function parseCCRecords() {
|
|
13901
14532
|
const homeDir = process.env.HOME ?? os2.homedir();
|
|
13902
|
-
const projectsDir =
|
|
14533
|
+
const projectsDir = path39.join(homeDir, ".claude", "projects");
|
|
13903
14534
|
let projectDirs;
|
|
13904
14535
|
try {
|
|
13905
|
-
projectDirs =
|
|
14536
|
+
projectDirs = fs39.readdirSync(projectsDir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => path39.join(projectsDir, d.name));
|
|
13906
14537
|
} catch {
|
|
13907
14538
|
return [];
|
|
13908
14539
|
}
|
|
@@ -13910,7 +14541,7 @@ function parseCCRecords() {
|
|
|
13910
14541
|
for (const dir of projectDirs) {
|
|
13911
14542
|
let files;
|
|
13912
14543
|
try {
|
|
13913
|
-
files =
|
|
14544
|
+
files = fs39.readdirSync(dir).filter((f) => f.endsWith(".jsonl")).map((f) => path39.join(dir, f));
|
|
13914
14545
|
} catch {
|
|
13915
14546
|
continue;
|
|
13916
14547
|
}
|
|
@@ -13938,10 +14569,10 @@ function parseLine2(line, lineNumber) {
|
|
|
13938
14569
|
}
|
|
13939
14570
|
}
|
|
13940
14571
|
function readAdoptionRecords(projectRoot) {
|
|
13941
|
-
const adoptionFile =
|
|
14572
|
+
const adoptionFile = path40.join(projectRoot, ".harness", "metrics", "adoption.jsonl");
|
|
13942
14573
|
let raw;
|
|
13943
14574
|
try {
|
|
13944
|
-
raw =
|
|
14575
|
+
raw = fs40.readFileSync(adoptionFile, "utf-8");
|
|
13945
14576
|
} catch {
|
|
13946
14577
|
return [];
|
|
13947
14578
|
}
|
|
@@ -14089,25 +14720,25 @@ function resolveStability(contentType) {
|
|
|
14089
14720
|
return STABILITY_LOOKUP[contentType] ?? "ephemeral";
|
|
14090
14721
|
}
|
|
14091
14722
|
function getOrCreateInstallId(projectRoot) {
|
|
14092
|
-
const harnessDir =
|
|
14093
|
-
const installIdFile =
|
|
14723
|
+
const harnessDir = path41.join(projectRoot, ".harness");
|
|
14724
|
+
const installIdFile = path41.join(harnessDir, ".install-id");
|
|
14094
14725
|
const UUID_V4_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
14095
14726
|
try {
|
|
14096
|
-
const existing =
|
|
14727
|
+
const existing = fs41.readFileSync(installIdFile, "utf-8").trim();
|
|
14097
14728
|
if (UUID_V4_RE.test(existing)) {
|
|
14098
14729
|
return existing;
|
|
14099
14730
|
}
|
|
14100
14731
|
} catch {
|
|
14101
14732
|
}
|
|
14102
14733
|
const id = crypto4.randomUUID();
|
|
14103
|
-
|
|
14104
|
-
|
|
14734
|
+
fs41.mkdirSync(harnessDir, { recursive: true });
|
|
14735
|
+
fs41.writeFileSync(installIdFile, id, { encoding: "utf-8", mode: 384 });
|
|
14105
14736
|
return id;
|
|
14106
14737
|
}
|
|
14107
14738
|
function parseIdentityFromTelemetryFile(filePath) {
|
|
14108
14739
|
const identity = {};
|
|
14109
14740
|
try {
|
|
14110
|
-
const raw =
|
|
14741
|
+
const raw = fs42.readFileSync(filePath, "utf-8");
|
|
14111
14742
|
const parsed = JSON.parse(raw);
|
|
14112
14743
|
const src = parsed?.identity;
|
|
14113
14744
|
if (!src || typeof src !== "object") return identity;
|
|
@@ -14120,7 +14751,7 @@ function parseIdentityFromTelemetryFile(filePath) {
|
|
|
14120
14751
|
}
|
|
14121
14752
|
function readProjectNameFallback(configPath) {
|
|
14122
14753
|
try {
|
|
14123
|
-
const raw =
|
|
14754
|
+
const raw = fs42.readFileSync(configPath, "utf-8");
|
|
14124
14755
|
const config = JSON.parse(raw);
|
|
14125
14756
|
return typeof config?.name === "string" ? config.name : void 0;
|
|
14126
14757
|
} catch {
|
|
@@ -14141,10 +14772,10 @@ function readGitAliasFallback(cwd) {
|
|
|
14141
14772
|
}
|
|
14142
14773
|
}
|
|
14143
14774
|
function readIdentity(projectRoot) {
|
|
14144
|
-
const filePath =
|
|
14775
|
+
const filePath = path42.join(projectRoot, ".harness", "telemetry.json");
|
|
14145
14776
|
const identity = parseIdentityFromTelemetryFile(filePath);
|
|
14146
14777
|
if (!identity.project) {
|
|
14147
|
-
const fallbackProject = readProjectNameFallback(
|
|
14778
|
+
const fallbackProject = readProjectNameFallback(path42.join(projectRoot, "harness.config.json"));
|
|
14148
14779
|
if (fallbackProject) identity.project = fallbackProject;
|
|
14149
14780
|
}
|
|
14150
14781
|
if (!identity.alias) {
|
|
@@ -14191,7 +14822,7 @@ function collectEvents(projectRoot, consent) {
|
|
|
14191
14822
|
);
|
|
14192
14823
|
}
|
|
14193
14824
|
function sleep3(ms) {
|
|
14194
|
-
return new Promise((
|
|
14825
|
+
return new Promise((resolve9) => setTimeout(resolve9, ms));
|
|
14195
14826
|
}
|
|
14196
14827
|
async function send(events, apiKey) {
|
|
14197
14828
|
if (events.length === 0) return;
|
|
@@ -14221,12 +14852,12 @@ function acquireCompoundLock(category, opts = {}) {
|
|
|
14221
14852
|
);
|
|
14222
14853
|
}
|
|
14223
14854
|
const cwd = opts.cwd ?? process.cwd();
|
|
14224
|
-
const lockDir =
|
|
14225
|
-
|
|
14226
|
-
const lockPath =
|
|
14855
|
+
const lockDir = path43.join(cwd, ".harness", "locks");
|
|
14856
|
+
fs43.mkdirSync(lockDir, { recursive: true });
|
|
14857
|
+
const lockPath = path43.join(lockDir, `compound-${category}.lock`);
|
|
14227
14858
|
let fd;
|
|
14228
14859
|
try {
|
|
14229
|
-
fd =
|
|
14860
|
+
fd = fs43.openSync(lockPath, "wx");
|
|
14230
14861
|
} catch (e) {
|
|
14231
14862
|
const err = e;
|
|
14232
14863
|
if (err.code === "EEXIST") {
|
|
@@ -14235,14 +14866,14 @@ function acquireCompoundLock(category, opts = {}) {
|
|
|
14235
14866
|
}
|
|
14236
14867
|
throw err;
|
|
14237
14868
|
}
|
|
14238
|
-
|
|
14239
|
-
|
|
14869
|
+
fs43.writeSync(fd, String(process.pid));
|
|
14870
|
+
fs43.closeSync(fd);
|
|
14240
14871
|
let released = false;
|
|
14241
14872
|
const release = () => {
|
|
14242
14873
|
if (released) return;
|
|
14243
14874
|
released = true;
|
|
14244
14875
|
try {
|
|
14245
|
-
|
|
14876
|
+
fs43.unlinkSync(lockPath);
|
|
14246
14877
|
} catch {
|
|
14247
14878
|
}
|
|
14248
14879
|
process.removeListener("exit", onExit);
|
|
@@ -14268,7 +14899,7 @@ function acquireCompoundLock(category, opts = {}) {
|
|
|
14268
14899
|
}
|
|
14269
14900
|
function readHolderPid(lockPath) {
|
|
14270
14901
|
try {
|
|
14271
|
-
const raw =
|
|
14902
|
+
const raw = fs43.readFileSync(lockPath, "utf-8").trim();
|
|
14272
14903
|
const n = Number.parseInt(raw, 10);
|
|
14273
14904
|
return Number.isFinite(n) ? n : -1;
|
|
14274
14905
|
} catch {
|
|
@@ -14293,10 +14924,10 @@ function assertSanitized(value) {
|
|
|
14293
14924
|
}
|
|
14294
14925
|
function writePulseConfig(config, opts) {
|
|
14295
14926
|
PulseConfigSchema.parse(config);
|
|
14296
|
-
if (!
|
|
14927
|
+
if (!fs44.existsSync(opts.configPath)) {
|
|
14297
14928
|
throw new Error(`harness.config.json not found at ${opts.configPath}`);
|
|
14298
14929
|
}
|
|
14299
|
-
const raw =
|
|
14930
|
+
const raw = fs44.readFileSync(opts.configPath, "utf-8");
|
|
14300
14931
|
let parsed;
|
|
14301
14932
|
try {
|
|
14302
14933
|
parsed = JSON.parse(raw);
|
|
@@ -14304,18 +14935,18 @@ function writePulseConfig(config, opts) {
|
|
|
14304
14935
|
throw new Error(`Invalid JSON in ${opts.configPath}: ${e.message}`, { cause: e });
|
|
14305
14936
|
}
|
|
14306
14937
|
const bakPath = `${opts.configPath}.bak`;
|
|
14307
|
-
if (!opts.skipBackup && !
|
|
14308
|
-
|
|
14938
|
+
if (!opts.skipBackup && !fs44.existsSync(bakPath)) {
|
|
14939
|
+
fs44.writeFileSync(bakPath, raw, "utf-8");
|
|
14309
14940
|
}
|
|
14310
14941
|
parsed.pulse = config;
|
|
14311
14942
|
const serialized = JSON.stringify(parsed, null, 2) + "\n";
|
|
14312
14943
|
const tmpPath = `${opts.configPath}.tmp-${process.pid}`;
|
|
14313
|
-
|
|
14944
|
+
fs44.writeFileSync(tmpPath, serialized, "utf-8");
|
|
14314
14945
|
try {
|
|
14315
|
-
|
|
14946
|
+
fs44.renameSync(tmpPath, opts.configPath);
|
|
14316
14947
|
} catch (e) {
|
|
14317
14948
|
try {
|
|
14318
|
-
|
|
14949
|
+
fs44.unlinkSync(tmpPath);
|
|
14319
14950
|
} catch {
|
|
14320
14951
|
}
|
|
14321
14952
|
throw e;
|
|
@@ -14323,12 +14954,12 @@ function writePulseConfig(config, opts) {
|
|
|
14323
14954
|
}
|
|
14324
14955
|
function seedFromStrategy(opts = {}) {
|
|
14325
14956
|
const cwd = opts.cwd ?? process.cwd();
|
|
14326
|
-
const strategyPath =
|
|
14957
|
+
const strategyPath = path44.join(cwd, "STRATEGY.md");
|
|
14327
14958
|
const warnings = [];
|
|
14328
|
-
if (!
|
|
14959
|
+
if (!fs45.existsSync(strategyPath)) {
|
|
14329
14960
|
return { name: null, keyMetrics: [], warnings: ["STRATEGY.md not found"] };
|
|
14330
14961
|
}
|
|
14331
|
-
const raw =
|
|
14962
|
+
const raw = fs45.readFileSync(strategyPath, "utf-8");
|
|
14332
14963
|
let name = null;
|
|
14333
14964
|
const fmMatch = /^---\s*\n([\s\S]*?)\n---\s*\n/.exec(raw);
|
|
14334
14965
|
const fm = fmMatch?.[1];
|
|
@@ -14480,7 +15111,7 @@ function loadTemplate() {
|
|
|
14480
15111
|
const url = import.meta?.url;
|
|
14481
15112
|
if (!url) return INLINE_TEMPLATE;
|
|
14482
15113
|
const here = dirname18(fileURLToPath(url));
|
|
14483
|
-
return
|
|
15114
|
+
return readFileSync38(join612(here, "template.md"), "utf-8");
|
|
14484
15115
|
} catch {
|
|
14485
15116
|
return INLINE_TEMPLATE;
|
|
14486
15117
|
}
|
|
@@ -14669,11 +15300,11 @@ async function computeHotspots(opts) {
|
|
|
14669
15300
|
}
|
|
14670
15301
|
const counts = /* @__PURE__ */ new Map();
|
|
14671
15302
|
for (const line of stdout.split("\n")) {
|
|
14672
|
-
const
|
|
14673
|
-
if (
|
|
14674
|
-
counts.set(
|
|
15303
|
+
const path46 = line.trim();
|
|
15304
|
+
if (path46.length === 0) continue;
|
|
15305
|
+
counts.set(path46, (counts.get(path46) ?? 0) + 1);
|
|
14675
15306
|
}
|
|
14676
|
-
return [...counts.entries()].filter(([, c]) => c > opts.threshold).map(([
|
|
15307
|
+
return [...counts.entries()].filter(([, c]) => c > opts.threshold).map(([path46, churn]) => ({ path: path46, churn })).sort((a, b) => b.churn - a.churn);
|
|
14677
15308
|
}
|
|
14678
15309
|
function tokenize(text) {
|
|
14679
15310
|
return new Set(
|
|
@@ -14683,12 +15314,12 @@ function tokenize(text) {
|
|
|
14683
15314
|
async function* walk22(dir) {
|
|
14684
15315
|
let entries;
|
|
14685
15316
|
try {
|
|
14686
|
-
entries = await
|
|
15317
|
+
entries = await fs46.readdir(dir, { withFileTypes: true });
|
|
14687
15318
|
} catch {
|
|
14688
15319
|
return;
|
|
14689
15320
|
}
|
|
14690
15321
|
for (const e of entries) {
|
|
14691
|
-
const p =
|
|
15322
|
+
const p = path45.join(dir, e.name);
|
|
14692
15323
|
if (e.isDirectory()) yield* walk22(p);
|
|
14693
15324
|
else if (e.isFile() && e.name.endsWith(".md")) yield p;
|
|
14694
15325
|
}
|
|
@@ -14697,11 +15328,11 @@ async function readDocumentedTokens(solutionsDir) {
|
|
|
14697
15328
|
const docs = [];
|
|
14698
15329
|
for (const [track, categories] of TRACK_CATEGORIES2) {
|
|
14699
15330
|
for (const category of categories) {
|
|
14700
|
-
const dir =
|
|
15331
|
+
const dir = path45.join(solutionsDir, track, category);
|
|
14701
15332
|
for await (const file of walk22(dir)) {
|
|
14702
|
-
const raw = await
|
|
15333
|
+
const raw = await fs46.readFile(file, "utf-8");
|
|
14703
15334
|
const m = /^#\s+(.+)$/m.exec(raw);
|
|
14704
|
-
const title = m?.[1] ??
|
|
15335
|
+
const title = m?.[1] ?? path45.basename(file, ".md");
|
|
14705
15336
|
docs.push(tokenize(title));
|
|
14706
15337
|
}
|
|
14707
15338
|
}
|
|
@@ -14764,16 +15395,20 @@ function assembleCandidateReport(input) {
|
|
|
14764
15395
|
}
|
|
14765
15396
|
return lines.join("\n");
|
|
14766
15397
|
}
|
|
14767
|
-
var VALID_TYPES, CONVENTIONAL_PATTERN, LOOKBACK_PATTERN, PulseDbSourceSchema, PulseSourcesSchema, PulseConfigSchema, BUG_TRACK_CATEGORIES, KNOWLEDGE_TRACK_CATEGORIES, ALL_SOLUTION_CATEGORIES, ISO_DATE, BugTrackSchema, KnowledgeTrackSchema, BaseFrontmatter, SolutionDocFrontmatterSchema, TRACK_CATEGORIES, DEFAULT_AGNIX_TIMEOUT_MS, HARNESS_AGNIX_DISABLE, HARNESS_AGNIX_BIN, CLAUDE_MD_WARN_BYTES, CLAUDE_MD_ERROR_BYTES, CONTEXT_FILES, MIN_DESCRIPTION_LENGTH, SETTINGS_PATHS, KNOWN_HOOK_EVENTS, MCP_SETTINGS_FILES, REQUIRED_SECTIONS, SETTINGS_FILES, KNOWN_AGNIX_TOOLS, DEFAULT_SECTIONS, DEFAULT_RATIOS, NODE_TYPE_TO_CATEGORY, SECTION_LEVEL_MAP, DEFAULT_LEVEL, DEFAULT_BUDGET, DEFAULT_LOADER_CONFIG, LEVEL_FRACTIONS, PHASE_PRIORITIES, ManifestSchema, BundleConstraintsSchema, BundleSchema, ContributionsSchema, LockfilePackageSchema, LockfileSchema, SharableLayerSchema, SharableForbiddenImportSchema, SharableBoundaryConfigSchema, SharableSecurityRulesSchema, VALID_SCOPES, LINE_PATTERN, START_PATTERN, END_PATTERN, CODE_BLOCK_LANGUAGES, DEFAULT_DRIFT_CONFIG, JS_EXT_FALLBACKS2, FILE_TYPES, EXPORT_TYPES, CONVENTION_DESCRIPTIONS, RULE_CHECKERS, EntropyAnalyzer, readFile62, writeFile32, unlink22, mkdir22, copyFile2, DEFAULT_FIX_CONFIG, EXPORT_TYPE_KEYWORD, ALWAYS_UNSAFE_TYPES, idCounter, DEAD_CODE_FIX_ACTIONS, MustExportRuleSchema, MustExportDefaultRuleSchema, NoExportRuleSchema, MustImportRuleSchema, NoImportRuleSchema, NamingRuleSchema, MaxExportsRuleSchema, MaxLinesRuleSchema, RequireJSDocRuleSchema, RuleSchema, ConfigPatternSchema, PatternConfigSchema, DriftConfigSchema, DeadCodeConfigSchema, EntropyConfigSchema, BaselineManager, BenchmarkRunner, RegressionDetector, SKIP_DIRS, SOURCE_EXTENSIONS, FUNCTION_DECL_RE, CONST_DECL_RE, CriticalPathResolver, NoOpTelemetryAdapter, NoOpExecutor, ConsoleSink, feedbackConfig, ChecklistBuilder, AgentActionEmitter, globalEmitter, FileSink, NoOpSink, CategorySnapshotSchema, TimelineSnapshotSchema, TimelineFileSchema, TrendLineSchema, TrendResultSchema, DEFAULT_STABILITY_THRESHOLDS, ALL_CATEGORIES, TimelineManager, ConfidenceTierSchema, RegressionResultSchema, DirectionSchema, CategoryForecastSchema, SpecImpactSignalsSchema, SpecImpactEstimateSchema, ContributingFeatureSchema, AdjustedForecastSchema, PredictionWarningSchema, StabilityForecastSchema, PredictionResultSchema, PredictionOptionsSchema, VALID_STATUSES, EM_DASH, VALID_PRIORITIES, ALL_CATEGORIES2, DIRECTION_THRESHOLD, PredictionEngine, DEFAULT_COEFFICIENTS, SpecImpactEstimator, EMPTY_HISTORY, ViolationHistoryManager, NORMALIZERS, MAX_SAMPLES, DEFAULT_WINDOW_WEEKS, DEFAULT_MIN_OCCURRENCES, FailureEntrySchema, HandoffSchema, GateCheckSchema, GateResultSchema, GateConfigSchema, HarnessStateSchema, DEFAULT_STATE, StreamInfoSchema, StreamIndexSchema, DEFAULT_STREAM_INDEX, HARNESS_DIR, STATE_FILE, LEARNINGS_FILE, FAILURES_FILE, HANDOFF_FILE, GATE_CONFIG_FILE, INDEX_FILE, SESSIONS_DIR, SESSION_INDEX_FILE, SUMMARY_FILE, SESSION_STATE_FILE, ARCHIVE_DIR, CONTENT_HASHES_FILE, EVENTS_FILE, STREAMS_DIR, STREAM_NAME_REGEX, branchCache, BRANCH_CACHE_TTL_MS, STATE_FILES, MAX_CACHE_ENTRIES, learningsCacheMap, WEIGHTS, LOCK_RETRIES, LOCK_BACKOFFS, PROMOTABLE_OUTCOMES, failuresCacheMap, FAILURE_LINE_REGEX, SAFE_GATE_COMMAND, SkillEventSchema, knownHashesCache, EVENT_FORMATTERS, RuleRegistry, DEFAULT_SECURITY_CONFIG, RuleOverrideSchema, SecurityConfigSchema, secretRules, injectionRules, xssRules, cryptoRules, pathTraversalRules, networkRules, deserializationRules, agentConfigRules, mcpRules, insecureDefaultsRules, sharpEdgesRules, nodeRules, expressRules, reactRules, goRules, SecurityScanner, hiddenUnicodePatterns, reRolingPatterns, permissionEscalationPatterns, encodedPayloadPatterns, indirectInjectionPatterns, contextManipulationPatterns, socialEngineeringPatterns, suspiciousPatterns, ALL_PATTERNS, DESTRUCTIVE_BASH, TAINT_DURATION_MS, DEFAULT_SESSION_ID, SecurityCategorySnapshotSchema, SupplyChainSnapshotSchema, SecurityTimelineSnapshotSchema, FindingLifecycleSchema, SecurityTimelineFileSchema, DirectionSchema2, SecurityTrendLineSchema, TrendAttributionSchema, SecurityTrendResultSchema, TimeToFixStatsSchema, TimeToFixResultSchema, EMPTY_SUPPLY_CHAIN, SecurityTimelineManager, ALL_CHECKS, STATUS_ICONS, CINotifier, ExclusionSet, PREFIX_PATTERNS, TEST_FILE_PATTERN, MD_FILE_PATTERN, ALL_DOMAINS, SECURITY_PATTERNS, JS_EXT_FALLBACKS22, SEVERITY_RANK, SEVERITY_ORDER, SEVERITY_LABELS, VALIDATED_BY_RANK, VALIDATION_SCORES, DOMAIN_BASELINES, FACTOR_WEIGHTS, EVIDENCE_SATURATION, CORROBORATED_AGREEMENT, STANDALONE_AGREEMENT, AGREEMENT_LINE_GAP, COMPLIANCE_DESCRIPTOR, EXPORT_RE, BUG_DETECTION_DESCRIPTOR, SECURITY_DESCRIPTOR, EVAL_PATTERN, SECRET_PATTERNS, SQL_CONCAT_PATTERN, SHELL_EXEC_PATTERN, ARCHITECTURE_DESCRIPTOR, LARGE_FILE_THRESHOLD, VIOLATION_FILE_RE, LEARNINGS_DESCRIPTOR, RELEVANCE_THRESHOLD, AGENT_DESCRIPTORS, AGENT_RUNNERS, DOWNGRADE_MAP, DEFAULT_PROVIDER_TIERS, SMALL_SUGGESTION_LINE_LIMIT, FILE_LINE_RANGE_PATTERN, FILE_LINE_PATTERN, FILE_ONLY_PATTERN, SECURITY_SENSITIVE_PATH, MIGRATION_PATH, TEST_PATH, CONFIG_PATH, STAGE_DOMAINS, EM_DASH2, STATUS_RANK, RETRY_DEFAULTS, GitHubIssuesSyncAdapter, syncMutex, PRIORITY_RANK, POSITION_WEIGHT, DEPENDENTS_WEIGHT, AFFINITY_WEIGHT, InteractionTypeSchema, QuestionSchema, ConfirmationSchema, TransitionSchema, EmitInteractionInputSchema, ProjectScanner, SHELL_TEMPLATE, STYLES, SCRIPTS, MockLLMService, llmService, ContentPipeline, BlueprintGenerator, TOKENS_PER_MILLION, fallback_default, LITELLM_PRICING_URL, CACHE_TTL_MS, STALENESS_WARNING_DAYS, MICRODOLLARS_PER_DOLLAR, TOKENS_PER_MILLION2, StructuralStrategy, DEFAULT_TOKEN_BUDGET, CHARS_PER_TOKEN, TRUNCATION_MARKER, TruncationStrategy, CompactionPipeline, STABILITY_LOOKUP, TIER_ORDER, AnthropicCacheAdapter, TIER_ORDER2, OpenAICacheAdapter, TIER_ORDER3, CACHED_CONTENT_MARKER, GeminiCacheAdapter, VERSION, POSTHOG_BATCH_URL, MAX_ATTEMPTS, TIMEOUT_MS, CompoundLockHeldError, KNOWN_CATEGORIES, ALLOWED_FIELD_KEYS, PII_TOKENS, PII_FIELD_DENYLIST, PII_LINE_RE, ALLOWED_SET, PulseAdapterAlreadyRegisteredError, REGISTRY, MOCK_ADAPTER_NAME, ALLOWED_FIELD_SET, adapter, LOOKBACK_RE, MAX_LINES, INLINE_TEMPLATE, TRUNCATION_MARKER2, execFileAsync, FIX_RE, EMPTY_REPO_RE, execFileAsync2, TRACK_CATEGORIES2, STOPWORDS, KEYWORD_TO_CATEGORY;
|
|
15398
|
+
var VALID_TYPES, CONVENTIONAL_PATTERN, LOOKBACK_PATTERN, PulseDbSourceSchema, PulseSourcesSchema, PulseConfigSchema, BUG_TRACK_CATEGORIES, KNOWLEDGE_TRACK_CATEGORIES, ALL_SOLUTION_CATEGORIES, ISO_DATE, BugTrackSchema, KnowledgeTrackSchema, BaseFrontmatter, SolutionDocFrontmatterSchema, TRACK_CATEGORIES, DEFAULT_AGNIX_TIMEOUT_MS, HARNESS_AGNIX_DISABLE, HARNESS_AGNIX_BIN, CLAUDE_MD_WARN_BYTES, CLAUDE_MD_ERROR_BYTES, CONTEXT_FILES, MIN_DESCRIPTION_LENGTH, SETTINGS_PATHS, KNOWN_HOOK_EVENTS, MCP_SETTINGS_FILES, REQUIRED_SECTIONS, SETTINGS_FILES, KNOWN_AGNIX_TOOLS, DEFAULT_SECTIONS, DEFAULT_RATIOS, NODE_TYPE_TO_CATEGORY, SECTION_LEVEL_MAP, DEFAULT_LEVEL, DEFAULT_BUDGET, DEFAULT_LOADER_CONFIG, LEVEL_FRACTIONS, PHASE_PRIORITIES, ManifestSchema, BundleConstraintsSchema, BundleSchema, ContributionsSchema, LockfilePackageSchema, LockfileSchema, SharableLayerSchema, SharableForbiddenImportSchema, SharableBoundaryConfigSchema, SharableSecurityRulesSchema, VALID_SCOPES, LINE_PATTERN, START_PATTERN, END_PATTERN, CODE_BLOCK_LANGUAGES, DEFAULT_DRIFT_CONFIG, JS_EXT_FALLBACKS2, FILE_TYPES, EXPORT_TYPES, CONVENTION_DESCRIPTIONS, RULE_CHECKERS, EntropyAnalyzer, readFile62, writeFile32, unlink22, mkdir22, copyFile2, DEFAULT_FIX_CONFIG, EXPORT_TYPE_KEYWORD, ALWAYS_UNSAFE_TYPES, idCounter, DEAD_CODE_FIX_ACTIONS, MustExportRuleSchema, MustExportDefaultRuleSchema, NoExportRuleSchema, MustImportRuleSchema, NoImportRuleSchema, NamingRuleSchema, MaxExportsRuleSchema, MaxLinesRuleSchema, RequireJSDocRuleSchema, RuleSchema, ConfigPatternSchema, PatternConfigSchema, DriftConfigSchema, DeadCodeConfigSchema, EntropyConfigSchema, BaselineManager, BenchmarkRunner, RegressionDetector, SKIP_DIRS, SOURCE_EXTENSIONS, FUNCTION_DECL_RE, CONST_DECL_RE, CriticalPathResolver, NoOpTelemetryAdapter, NoOpExecutor, ConsoleSink, feedbackConfig, ChecklistBuilder, AgentActionEmitter, globalEmitter, FileSink, NoOpSink, CategorySnapshotSchema, TimelineSnapshotSchema, TimelineFileSchema, TrendLineSchema, TrendResultSchema, DEFAULT_STABILITY_THRESHOLDS, ALL_CATEGORIES, TimelineManager, ConfidenceTierSchema, RegressionResultSchema, DirectionSchema, CategoryForecastSchema, SpecImpactSignalsSchema, SpecImpactEstimateSchema, ContributingFeatureSchema, AdjustedForecastSchema, PredictionWarningSchema, StabilityForecastSchema, PredictionResultSchema, PredictionOptionsSchema, VALID_STATUSES, EM_DASH, VALID_PRIORITIES, ALL_CATEGORIES2, DIRECTION_THRESHOLD, PredictionEngine, DEFAULT_COEFFICIENTS, SpecImpactEstimator, EMPTY_HISTORY, ViolationHistoryManager, NORMALIZERS, MAX_SAMPLES, DEFAULT_WINDOW_WEEKS, DEFAULT_MIN_OCCURRENCES, FailureEntrySchema, HandoffSchema, GateCheckSchema, GateResultSchema, GateConfigSchema, HarnessStateSchema, DEFAULT_STATE, StreamInfoSchema, StreamIndexSchema, DEFAULT_STREAM_INDEX, HARNESS_DIR, STATE_FILE, LEARNINGS_FILE, FAILURES_FILE, HANDOFF_FILE, GATE_CONFIG_FILE, INDEX_FILE, SESSIONS_DIR, SESSION_INDEX_FILE, SUMMARY_FILE, SESSION_STATE_FILE, ARCHIVE_DIR, CONTENT_HASHES_FILE, EVENTS_FILE, STREAMS_DIR, STREAM_NAME_REGEX, branchCache, BRANCH_CACHE_TTL_MS, STATE_FILES, MAX_CACHE_ENTRIES, learningsCacheMap, WEIGHTS, LOCK_RETRIES, LOCK_BACKOFFS, PROMOTABLE_OUTCOMES, failuresCacheMap, FAILURE_LINE_REGEX, SAFE_GATE_COMMAND, SkillEventSchema, knownHashesCache, EVENT_FORMATTERS, RuleRegistry, DEFAULT_SECURITY_CONFIG, RuleOverrideSchema, SecurityConfigSchema, secretRules, injectionRules, xssRules, cryptoRules, pathTraversalRules, networkRules, deserializationRules, agentConfigRules, mcpRules, insecureDefaultsRules, sharpEdgesRules, nodeRules, expressRules, reactRules, goRules, SecurityScanner, hiddenUnicodePatterns, reRolingPatterns, permissionEscalationPatterns, encodedPayloadPatterns, indirectInjectionPatterns, contextManipulationPatterns, socialEngineeringPatterns, suspiciousPatterns, ALL_PATTERNS, DESTRUCTIVE_BASH, TAINT_DURATION_MS, DEFAULT_SESSION_ID, SecurityCategorySnapshotSchema, SupplyChainSnapshotSchema, SecurityTimelineSnapshotSchema, FindingLifecycleSchema, SecurityTimelineFileSchema, DirectionSchema2, SecurityTrendLineSchema, TrendAttributionSchema, SecurityTrendResultSchema, TimeToFixStatsSchema, TimeToFixResultSchema, EMPTY_SUPPLY_CHAIN, SecurityTimelineManager, ALL_CHECKS, STATUS_ICONS, CINotifier, ExclusionSet, PREFIX_PATTERNS, TEST_FILE_PATTERN, MD_FILE_PATTERN, ALL_DOMAINS, SECURITY_PATTERNS, JS_EXT_FALLBACKS22, SEVERITY_RANK, SEVERITY_ORDER, SEVERITY_LABELS, VALIDATED_BY_RANK, VALIDATION_SCORES, DOMAIN_BASELINES, FACTOR_WEIGHTS, EVIDENCE_SATURATION, CORROBORATED_AGREEMENT, STANDALONE_AGREEMENT, AGREEMENT_LINE_GAP, COMPLIANCE_DESCRIPTOR, EXPORT_RE, BUG_DETECTION_DESCRIPTOR, SECURITY_DESCRIPTOR, EVAL_PATTERN, SECRET_PATTERNS, SQL_CONCAT_PATTERN, SHELL_EXEC_PATTERN, ARCHITECTURE_DESCRIPTOR, LARGE_FILE_THRESHOLD, VIOLATION_FILE_RE, LEARNINGS_DESCRIPTOR, RELEVANCE_THRESHOLD, AGENT_DESCRIPTORS, AGENT_RUNNERS, DOWNGRADE_MAP, DEFAULT_PROVIDER_TIERS, SMALL_SUGGESTION_LINE_LIMIT, FILE_LINE_RANGE_PATTERN, FILE_LINE_PATTERN, FILE_ONLY_PATTERN, SECURITY_SENSITIVE_PATH, MIGRATION_PATH, TEST_PATH, CONFIG_PATH, STAGE_DOMAINS, EM_DASH2, STATUS_RANK, RETRY_DEFAULTS, GitHubIssuesSyncAdapter, syncMutex, PRIORITY_RANK, PRIORITY_RANK2, POSITION_WEIGHT, DEPENDENTS_WEIGHT, AFFINITY_WEIGHT, ConflictError, DEFAULTS, GitHubHttp, ETagStore, START_MARKER, END_MARKER, SERIALIZER_FIELDS, GitHubIssuesTrackerAdapter, migrate_exports, HASH_RE, InteractionTypeSchema, QuestionSchema, ConfirmationSchema, TransitionSchema, EmitInteractionInputSchema, ProjectScanner, SHELL_TEMPLATE, STYLES, SCRIPTS, MockLLMService, llmService, ContentPipeline, BlueprintGenerator, TOKENS_PER_MILLION, fallback_default, LITELLM_PRICING_URL, CACHE_TTL_MS, STALENESS_WARNING_DAYS, MICRODOLLARS_PER_DOLLAR, TOKENS_PER_MILLION2, StructuralStrategy, DEFAULT_TOKEN_BUDGET, CHARS_PER_TOKEN, TRUNCATION_MARKER, TruncationStrategy, CompactionPipeline, STABILITY_LOOKUP, TIER_ORDER, AnthropicCacheAdapter, TIER_ORDER2, OpenAICacheAdapter, TIER_ORDER3, CACHED_CONTENT_MARKER, GeminiCacheAdapter, VERSION, POSTHOG_BATCH_URL, MAX_ATTEMPTS, TIMEOUT_MS, CompoundLockHeldError, KNOWN_CATEGORIES, ALLOWED_FIELD_KEYS, PII_TOKENS, PII_FIELD_DENYLIST, PII_LINE_RE, ALLOWED_SET, PulseAdapterAlreadyRegisteredError, REGISTRY, MOCK_ADAPTER_NAME, ALLOWED_FIELD_SET, adapter, LOOKBACK_RE, MAX_LINES, INLINE_TEMPLATE, TRUNCATION_MARKER2, execFileAsync, FIX_RE, EMPTY_REPO_RE, execFileAsync2, TRACK_CATEGORIES2, STOPWORDS, KEYWORD_TO_CATEGORY;
|
|
14768
15399
|
var init_dist3 = __esm({
|
|
14769
15400
|
"../core/dist/index.mjs"() {
|
|
14770
|
-
|
|
15401
|
+
init_chunk_NC4RPKD4();
|
|
14771
15402
|
init_dist();
|
|
14772
15403
|
init_dist();
|
|
14773
15404
|
init_dist();
|
|
14774
15405
|
init_dist2();
|
|
14775
15406
|
init_dist();
|
|
14776
15407
|
init_dist();
|
|
15408
|
+
init_dist();
|
|
15409
|
+
init_dist();
|
|
15410
|
+
init_dist();
|
|
15411
|
+
init_dist();
|
|
14777
15412
|
init_dist2();
|
|
14778
15413
|
VALID_TYPES = [
|
|
14779
15414
|
"feat",
|
|
@@ -15356,11 +15991,11 @@ var init_dist3 = __esm({
|
|
|
15356
15991
|
return detectPatternViolations(snapshotResult.value, config);
|
|
15357
15992
|
}
|
|
15358
15993
|
};
|
|
15359
|
-
readFile62 = promisify2(
|
|
15360
|
-
writeFile32 = promisify2(
|
|
15361
|
-
unlink22 = promisify2(
|
|
15362
|
-
mkdir22 = promisify2(
|
|
15363
|
-
copyFile2 = promisify2(
|
|
15994
|
+
readFile62 = promisify2(fs6.readFile);
|
|
15995
|
+
writeFile32 = promisify2(fs6.writeFile);
|
|
15996
|
+
unlink22 = promisify2(fs6.unlink);
|
|
15997
|
+
mkdir22 = promisify2(fs6.mkdir);
|
|
15998
|
+
copyFile2 = promisify2(fs6.copyFile);
|
|
15364
15999
|
DEFAULT_FIX_CONFIG = {
|
|
15365
16000
|
dryRun: false,
|
|
15366
16001
|
fixTypes: ["unused-imports", "dead-files"],
|
|
@@ -15482,14 +16117,14 @@ var init_dist3 = __esm({
|
|
|
15482
16117
|
BaselineManager = class {
|
|
15483
16118
|
baselinesPath;
|
|
15484
16119
|
constructor(projectRoot) {
|
|
15485
|
-
this.baselinesPath =
|
|
16120
|
+
this.baselinesPath = join18(projectRoot, ".harness", "perf", "baselines.json");
|
|
15486
16121
|
}
|
|
15487
16122
|
/**
|
|
15488
16123
|
* Load the baselines file from disk.
|
|
15489
16124
|
* Returns null if the file does not exist or contains invalid JSON.
|
|
15490
16125
|
*/
|
|
15491
16126
|
load() {
|
|
15492
|
-
if (!
|
|
16127
|
+
if (!existsSync12(this.baselinesPath)) {
|
|
15493
16128
|
return null;
|
|
15494
16129
|
}
|
|
15495
16130
|
try {
|
|
@@ -15523,7 +16158,7 @@ var init_dist3 = __esm({
|
|
|
15523
16158
|
benchmarks
|
|
15524
16159
|
};
|
|
15525
16160
|
const dir = dirname8(this.baselinesPath);
|
|
15526
|
-
if (!
|
|
16161
|
+
if (!existsSync12(dir)) {
|
|
15527
16162
|
mkdirSync2(dir, { recursive: true });
|
|
15528
16163
|
}
|
|
15529
16164
|
writeFileSync2(this.baselinesPath, JSON.stringify(file, null, 2));
|
|
@@ -15757,16 +16392,16 @@ var init_dist3 = __esm({
|
|
|
15757
16392
|
walkDir(dir, entries) {
|
|
15758
16393
|
let items;
|
|
15759
16394
|
try {
|
|
15760
|
-
items =
|
|
16395
|
+
items = fs7.readdirSync(dir, { withFileTypes: true });
|
|
15761
16396
|
} catch {
|
|
15762
16397
|
return;
|
|
15763
16398
|
}
|
|
15764
16399
|
for (const item of items) {
|
|
15765
16400
|
if (item.isDirectory()) {
|
|
15766
16401
|
if (SKIP_DIRS.has(item.name)) continue;
|
|
15767
|
-
this.walkDir(
|
|
15768
|
-
} else if (item.isFile() && SOURCE_EXTENSIONS.has(
|
|
15769
|
-
this.scanFile(
|
|
16402
|
+
this.walkDir(path4.join(dir, item.name), entries);
|
|
16403
|
+
} else if (item.isFile() && SOURCE_EXTENSIONS.has(path4.extname(item.name))) {
|
|
16404
|
+
this.scanFile(path4.join(dir, item.name), entries);
|
|
15770
16405
|
}
|
|
15771
16406
|
}
|
|
15772
16407
|
}
|
|
@@ -15781,12 +16416,12 @@ var init_dist3 = __esm({
|
|
|
15781
16416
|
scanFile(filePath, entries) {
|
|
15782
16417
|
let content;
|
|
15783
16418
|
try {
|
|
15784
|
-
content =
|
|
16419
|
+
content = fs7.readFileSync(filePath, "utf-8");
|
|
15785
16420
|
} catch {
|
|
15786
16421
|
return;
|
|
15787
16422
|
}
|
|
15788
16423
|
const lines = content.split("\n");
|
|
15789
|
-
const relativePath =
|
|
16424
|
+
const relativePath = path4.relative(this.projectRoot, filePath).replace(/\\/g, "/");
|
|
15790
16425
|
for (let i = 0; i < lines.length; i++) {
|
|
15791
16426
|
if (!lines[i].includes("@perf-critical")) continue;
|
|
15792
16427
|
const fnName = this.resolveFunctionName(lines, i + 1);
|
|
@@ -16126,7 +16761,7 @@ var init_dist3 = __esm({
|
|
|
16126
16761
|
ensureDirectory() {
|
|
16127
16762
|
if (!this.initialized) {
|
|
16128
16763
|
const dir = dirname9(this.filePath);
|
|
16129
|
-
if (!
|
|
16764
|
+
if (!existsSync13(dir)) {
|
|
16130
16765
|
mkdirSync22(dir, { recursive: true });
|
|
16131
16766
|
}
|
|
16132
16767
|
this.initialized = true;
|
|
@@ -16157,7 +16792,7 @@ var init_dist3 = __esm({
|
|
|
16157
16792
|
this.ensureDirectory();
|
|
16158
16793
|
const content = this.buffer.join("");
|
|
16159
16794
|
this.buffer = [];
|
|
16160
|
-
if (this.options.mode === "overwrite" && !
|
|
16795
|
+
if (this.options.mode === "overwrite" && !existsSync13(this.filePath)) {
|
|
16161
16796
|
writeFileSync22(this.filePath, content);
|
|
16162
16797
|
} else {
|
|
16163
16798
|
appendFileSync(this.filePath, content);
|
|
@@ -16239,14 +16874,14 @@ var init_dist3 = __esm({
|
|
|
16239
16874
|
TimelineManager = class {
|
|
16240
16875
|
timelinePath;
|
|
16241
16876
|
constructor(rootDir) {
|
|
16242
|
-
this.timelinePath =
|
|
16877
|
+
this.timelinePath = join20(rootDir, ".harness", "arch", "timeline.json");
|
|
16243
16878
|
}
|
|
16244
16879
|
/**
|
|
16245
16880
|
* Load timeline from disk.
|
|
16246
16881
|
* Returns empty TimelineFile if file does not exist or is invalid.
|
|
16247
16882
|
*/
|
|
16248
16883
|
load() {
|
|
16249
|
-
if (!
|
|
16884
|
+
if (!existsSync14(this.timelinePath)) {
|
|
16250
16885
|
return { version: 1, snapshots: [] };
|
|
16251
16886
|
}
|
|
16252
16887
|
try {
|
|
@@ -16272,7 +16907,7 @@ var init_dist3 = __esm({
|
|
|
16272
16907
|
*/
|
|
16273
16908
|
save(timeline) {
|
|
16274
16909
|
const dir = dirname10(this.timelinePath);
|
|
16275
|
-
if (!
|
|
16910
|
+
if (!existsSync14(dir)) {
|
|
16276
16911
|
mkdirSync3(dir, { recursive: true });
|
|
16277
16912
|
}
|
|
16278
16913
|
const tmp = this.timelinePath + "." + randomBytes2(4).toString("hex") + ".tmp";
|
|
@@ -16829,8 +17464,8 @@ var init_dist3 = __esm({
|
|
|
16829
17464
|
return null;
|
|
16830
17465
|
}
|
|
16831
17466
|
try {
|
|
16832
|
-
const roadmapPath =
|
|
16833
|
-
const raw =
|
|
17467
|
+
const roadmapPath = path5.join(this.rootDir, "roadmap.md");
|
|
17468
|
+
const raw = fs8.readFileSync(roadmapPath, "utf-8");
|
|
16834
17469
|
const parseResult = parseRoadmap(raw);
|
|
16835
17470
|
if (!parseResult.ok) return null;
|
|
16836
17471
|
const features = parseResult.value.milestones.flatMap(
|
|
@@ -16865,8 +17500,8 @@ var init_dist3 = __esm({
|
|
|
16865
17500
|
* @param specPath - Relative path from rootDir to the spec file.
|
|
16866
17501
|
*/
|
|
16867
17502
|
estimate(specPath) {
|
|
16868
|
-
const absolutePath =
|
|
16869
|
-
const content =
|
|
17503
|
+
const absolutePath = path6.join(this.rootDir, specPath);
|
|
17504
|
+
const content = fs9.readFileSync(absolutePath, "utf-8");
|
|
16870
17505
|
const newFileCount = this.extractNewFileCount(content);
|
|
16871
17506
|
const affectedLayers = this.extractAffectedLayers(content);
|
|
16872
17507
|
const newDependencies = this.extractNewDependencies(content);
|
|
@@ -16878,7 +17513,7 @@ var init_dist3 = __esm({
|
|
|
16878
17513
|
phaseCount
|
|
16879
17514
|
);
|
|
16880
17515
|
const h1Match = content.match(/^#\s+(.+)$/m);
|
|
16881
|
-
const featureName = h1Match ? h1Match[1].trim() :
|
|
17516
|
+
const featureName = h1Match ? h1Match[1].trim() : path6.basename(specPath, ".md");
|
|
16882
17517
|
return {
|
|
16883
17518
|
specPath,
|
|
16884
17519
|
featureName,
|
|
@@ -16899,8 +17534,8 @@ var init_dist3 = __esm({
|
|
|
16899
17534
|
const results = [];
|
|
16900
17535
|
for (const feature of features) {
|
|
16901
17536
|
if (!feature.spec) continue;
|
|
16902
|
-
const absolutePath =
|
|
16903
|
-
if (!
|
|
17537
|
+
const absolutePath = path6.join(this.rootDir, feature.spec);
|
|
17538
|
+
if (!fs9.existsSync(absolutePath)) continue;
|
|
16904
17539
|
const estimate = this.estimate(feature.spec);
|
|
16905
17540
|
results.push({ ...estimate, featureName: feature.name });
|
|
16906
17541
|
}
|
|
@@ -16928,8 +17563,8 @@ var init_dist3 = __esm({
|
|
|
16928
17563
|
}
|
|
16929
17564
|
let count = 0;
|
|
16930
17565
|
for (const fp of filePaths) {
|
|
16931
|
-
const absolute =
|
|
16932
|
-
if (!
|
|
17566
|
+
const absolute = path6.join(this.rootDir, fp);
|
|
17567
|
+
if (!fs9.existsSync(absolute)) {
|
|
16933
17568
|
count++;
|
|
16934
17569
|
}
|
|
16935
17570
|
}
|
|
@@ -16999,8 +17634,8 @@ var init_dist3 = __esm({
|
|
|
16999
17634
|
// --- Private: Config Loading ---
|
|
17000
17635
|
loadLayerNames() {
|
|
17001
17636
|
try {
|
|
17002
|
-
const configPath =
|
|
17003
|
-
const raw =
|
|
17637
|
+
const configPath = path6.join(this.rootDir, "harness.config.json");
|
|
17638
|
+
const raw = fs9.readFileSync(configPath, "utf-8");
|
|
17004
17639
|
const config = JSON.parse(raw);
|
|
17005
17640
|
return (config.layers ?? []).map((l) => l.name);
|
|
17006
17641
|
} catch {
|
|
@@ -17019,8 +17654,8 @@ var init_dist3 = __esm({
|
|
|
17019
17654
|
historyPath;
|
|
17020
17655
|
/** Load history from disk, returning empty history if file doesn't exist. */
|
|
17021
17656
|
load() {
|
|
17022
|
-
if (!
|
|
17023
|
-
const raw =
|
|
17657
|
+
if (!fs10.existsSync(this.historyPath)) return { ...EMPTY_HISTORY, snapshots: [] };
|
|
17658
|
+
const raw = fs10.readFileSync(this.historyPath, "utf-8");
|
|
17024
17659
|
return JSON.parse(raw);
|
|
17025
17660
|
}
|
|
17026
17661
|
/** Append a timestamped snapshot of current violations. */
|
|
@@ -17034,18 +17669,18 @@ var init_dist3 = __esm({
|
|
|
17034
17669
|
}
|
|
17035
17670
|
/** Prune snapshots older than retentionDays. */
|
|
17036
17671
|
prune(retentionDays) {
|
|
17037
|
-
if (!
|
|
17672
|
+
if (!fs10.existsSync(this.historyPath)) return;
|
|
17038
17673
|
const history = this.load();
|
|
17039
17674
|
const cutoff = Date.now() - retentionDays * 24 * 60 * 60 * 1e3;
|
|
17040
17675
|
history.snapshots = history.snapshots.filter((s) => new Date(s.timestamp).getTime() >= cutoff);
|
|
17041
17676
|
this.write(history);
|
|
17042
17677
|
}
|
|
17043
17678
|
write(history) {
|
|
17044
|
-
const dir =
|
|
17045
|
-
if (!
|
|
17046
|
-
|
|
17679
|
+
const dir = path7.dirname(this.historyPath);
|
|
17680
|
+
if (!fs10.existsSync(dir)) {
|
|
17681
|
+
fs10.mkdirSync(dir, { recursive: true });
|
|
17047
17682
|
}
|
|
17048
|
-
|
|
17683
|
+
fs10.writeFileSync(this.historyPath, JSON.stringify(history, null, 2), "utf-8");
|
|
17049
17684
|
}
|
|
17050
17685
|
};
|
|
17051
17686
|
NORMALIZERS = {
|
|
@@ -18047,7 +18682,7 @@ var init_dist3 = __esm({
|
|
|
18047
18682
|
}
|
|
18048
18683
|
async scanFile(filePath) {
|
|
18049
18684
|
if (!this.config.enabled) return [];
|
|
18050
|
-
const content = await
|
|
18685
|
+
const content = await fs28.readFile(filePath, "utf-8");
|
|
18051
18686
|
return this.scanContentForFile(content, filePath, 1);
|
|
18052
18687
|
}
|
|
18053
18688
|
scanContentForFile(content, filePath, startLine = 1) {
|
|
@@ -18452,7 +19087,7 @@ var init_dist3 = __esm({
|
|
|
18452
19087
|
timelinePath;
|
|
18453
19088
|
constructor(rootDir) {
|
|
18454
19089
|
this.rootDir = rootDir;
|
|
18455
|
-
this.timelinePath =
|
|
19090
|
+
this.timelinePath = join40(rootDir, ".harness", "security", "timeline.json");
|
|
18456
19091
|
}
|
|
18457
19092
|
/**
|
|
18458
19093
|
* Load timeline from disk.
|
|
@@ -18463,7 +19098,7 @@ var init_dist3 = __esm({
|
|
|
18463
19098
|
* fixup is one-shot.
|
|
18464
19099
|
*/
|
|
18465
19100
|
load() {
|
|
18466
|
-
if (!
|
|
19101
|
+
if (!existsSync342(this.timelinePath)) {
|
|
18467
19102
|
return { version: 1, snapshots: [], findingLifecycles: [] };
|
|
18468
19103
|
}
|
|
18469
19104
|
try {
|
|
@@ -18497,7 +19132,7 @@ var init_dist3 = __esm({
|
|
|
18497
19132
|
*/
|
|
18498
19133
|
save(timeline) {
|
|
18499
19134
|
const dir = dirname13(this.timelinePath);
|
|
18500
|
-
if (!
|
|
19135
|
+
if (!existsSync342(dir)) {
|
|
18501
19136
|
mkdirSync15(dir, { recursive: true });
|
|
18502
19137
|
}
|
|
18503
19138
|
const tmp = this.timelinePath + "." + randomBytes22(4).toString("hex") + ".tmp";
|
|
@@ -19511,7 +20146,8 @@ var init_dist3 = __esm({
|
|
|
19511
20146
|
}
|
|
19512
20147
|
};
|
|
19513
20148
|
syncMutex = Promise.resolve();
|
|
19514
|
-
PRIORITY_RANK = {
|
|
20149
|
+
PRIORITY_RANK = { P0: 0, P1: 1, P2: 2, P3: 3 };
|
|
20150
|
+
PRIORITY_RANK2 = {
|
|
19515
20151
|
P0: 0,
|
|
19516
20152
|
P1: 1,
|
|
19517
20153
|
P2: 2,
|
|
@@ -19520,6 +20156,586 @@ var init_dist3 = __esm({
|
|
|
19520
20156
|
POSITION_WEIGHT = 0.5;
|
|
19521
20157
|
DEPENDENTS_WEIGHT = 0.3;
|
|
19522
20158
|
AFFINITY_WEIGHT = 0.2;
|
|
20159
|
+
ConflictError = class extends Error {
|
|
20160
|
+
code = "TRACKER_CONFLICT";
|
|
20161
|
+
externalId;
|
|
20162
|
+
diff;
|
|
20163
|
+
serverUpdatedAt;
|
|
20164
|
+
constructor(externalId, diff2, serverUpdatedAt = null, message) {
|
|
20165
|
+
super(message ?? `Conflict on ${externalId}: ${Object.keys(diff2).join(", ")}`);
|
|
20166
|
+
this.name = "ConflictError";
|
|
20167
|
+
this.externalId = externalId;
|
|
20168
|
+
this.diff = diff2;
|
|
20169
|
+
this.serverUpdatedAt = serverUpdatedAt;
|
|
20170
|
+
}
|
|
20171
|
+
};
|
|
20172
|
+
DEFAULTS = { maxRetries: 5, baseDelayMs: 1e3 };
|
|
20173
|
+
GitHubHttp = class {
|
|
20174
|
+
token;
|
|
20175
|
+
fetchFn;
|
|
20176
|
+
apiBase;
|
|
20177
|
+
retryOpts;
|
|
20178
|
+
constructor(opts) {
|
|
20179
|
+
this.token = opts.token;
|
|
20180
|
+
this.fetchFn = opts.fetchFn ?? globalThis.fetch;
|
|
20181
|
+
this.apiBase = opts.apiBase ?? "https://api.github.com";
|
|
20182
|
+
this.retryOpts = {
|
|
20183
|
+
maxRetries: opts.maxRetries ?? DEFAULTS.maxRetries,
|
|
20184
|
+
baseDelayMs: opts.baseDelayMs ?? DEFAULTS.baseDelayMs
|
|
20185
|
+
};
|
|
20186
|
+
}
|
|
20187
|
+
headers(extra) {
|
|
20188
|
+
return {
|
|
20189
|
+
Authorization: `Bearer ${this.token}`,
|
|
20190
|
+
Accept: "application/vnd.github+json",
|
|
20191
|
+
"Content-Type": "application/json",
|
|
20192
|
+
"X-GitHub-Api-Version": "2022-11-28",
|
|
20193
|
+
...extra ?? {}
|
|
20194
|
+
};
|
|
20195
|
+
}
|
|
20196
|
+
async request(url, init) {
|
|
20197
|
+
const { extraHeaders, ...rest } = init;
|
|
20198
|
+
const merged = {
|
|
20199
|
+
...rest,
|
|
20200
|
+
headers: this.headers(extraHeaders)
|
|
20201
|
+
};
|
|
20202
|
+
return this.fetchWithRetry(url, merged);
|
|
20203
|
+
}
|
|
20204
|
+
async fetchWithRetry(url, init) {
|
|
20205
|
+
let last;
|
|
20206
|
+
for (let attempt = 0; attempt <= this.retryOpts.maxRetries; attempt++) {
|
|
20207
|
+
const res = await this.fetchFn(url, init);
|
|
20208
|
+
if (res.status !== 403 && res.status !== 429 && res.status < 500) return res;
|
|
20209
|
+
last = res;
|
|
20210
|
+
if (attempt === this.retryOpts.maxRetries) break;
|
|
20211
|
+
const retryAfter = res.headers.get("Retry-After");
|
|
20212
|
+
let delayMs;
|
|
20213
|
+
if (retryAfter) {
|
|
20214
|
+
const seconds = parseInt(retryAfter, 10);
|
|
20215
|
+
delayMs = isNaN(seconds) ? this.retryOpts.baseDelayMs : seconds * 1e3;
|
|
20216
|
+
} else {
|
|
20217
|
+
delayMs = this.retryOpts.baseDelayMs * Math.pow(2, attempt) + Math.random() * 500;
|
|
20218
|
+
}
|
|
20219
|
+
await new Promise((r) => setTimeout(r, delayMs));
|
|
20220
|
+
}
|
|
20221
|
+
return last;
|
|
20222
|
+
}
|
|
20223
|
+
/**
|
|
20224
|
+
* Walk all pages of a paginated GET. Stops when a page returns < perPage items.
|
|
20225
|
+
*
|
|
20226
|
+
* Status semantics:
|
|
20227
|
+
* - `status: 304` is returned ONLY when the first page returned 304 (the
|
|
20228
|
+
* server confirmed the caller's If-None-Match matched and there are no
|
|
20229
|
+
* items to read). Callers may then serve cached data.
|
|
20230
|
+
* - `status: 200` is returned in all other terminal cases, including the
|
|
20231
|
+
* mid-walk case where page 1 returned 200 (with items) and a later page
|
|
20232
|
+
* returned 304. Mid-walk 304 is treated as "no further items"; already-
|
|
20233
|
+
* accumulated items are preserved and `lastEtag` is the latest ETag seen.
|
|
20234
|
+
*/
|
|
20235
|
+
async paginate(buildUrl, perPage = 100, extraHeaders) {
|
|
20236
|
+
const items = [];
|
|
20237
|
+
let page = 1;
|
|
20238
|
+
let lastEtag = null;
|
|
20239
|
+
let status = 200;
|
|
20240
|
+
for (; ; ) {
|
|
20241
|
+
const init = { method: "GET" };
|
|
20242
|
+
if (extraHeaders) init.extraHeaders = extraHeaders;
|
|
20243
|
+
const res = await this.request(buildUrl(page), init);
|
|
20244
|
+
const etag = res.headers.get("ETag");
|
|
20245
|
+
if (res.status === 304) {
|
|
20246
|
+
if (page === 1) return { items, lastEtag: etag, status: 304 };
|
|
20247
|
+
if (etag) lastEtag = etag;
|
|
20248
|
+
break;
|
|
20249
|
+
}
|
|
20250
|
+
if (!res.ok) {
|
|
20251
|
+
throw new Error(`GitHub ${res.status}: ${await res.text()}`);
|
|
20252
|
+
}
|
|
20253
|
+
const data = await res.json();
|
|
20254
|
+
items.push(...data);
|
|
20255
|
+
lastEtag = etag;
|
|
20256
|
+
status = res.status;
|
|
20257
|
+
if (data.length < perPage) break;
|
|
20258
|
+
page++;
|
|
20259
|
+
}
|
|
20260
|
+
return { items, lastEtag, status };
|
|
20261
|
+
}
|
|
20262
|
+
};
|
|
20263
|
+
ETagStore = class {
|
|
20264
|
+
max;
|
|
20265
|
+
cache = /* @__PURE__ */ new Map();
|
|
20266
|
+
constructor(max = 500) {
|
|
20267
|
+
this.max = max;
|
|
20268
|
+
}
|
|
20269
|
+
get(key) {
|
|
20270
|
+
const entry = this.cache.get(key);
|
|
20271
|
+
if (!entry) return null;
|
|
20272
|
+
this.cache.delete(key);
|
|
20273
|
+
this.cache.set(key, entry);
|
|
20274
|
+
return entry;
|
|
20275
|
+
}
|
|
20276
|
+
set(key, etag, data) {
|
|
20277
|
+
if (this.cache.has(key)) this.cache.delete(key);
|
|
20278
|
+
this.cache.set(key, { etag, data });
|
|
20279
|
+
if (this.cache.size > this.max) {
|
|
20280
|
+
const oldestKey = this.cache.keys().next().value;
|
|
20281
|
+
if (oldestKey !== void 0) this.cache.delete(oldestKey);
|
|
20282
|
+
}
|
|
20283
|
+
}
|
|
20284
|
+
invalidate(key) {
|
|
20285
|
+
this.cache.delete(key);
|
|
20286
|
+
}
|
|
20287
|
+
invalidateAll() {
|
|
20288
|
+
this.cache.clear();
|
|
20289
|
+
}
|
|
20290
|
+
/** Invalidate all keys matching a prefix. Used when writes invalidate `list:*`. */
|
|
20291
|
+
invalidatePrefix(prefix) {
|
|
20292
|
+
for (const key of [...this.cache.keys()]) {
|
|
20293
|
+
if (key.startsWith(prefix)) this.cache.delete(key);
|
|
20294
|
+
}
|
|
20295
|
+
}
|
|
20296
|
+
get size() {
|
|
20297
|
+
return this.cache.size;
|
|
20298
|
+
}
|
|
20299
|
+
};
|
|
20300
|
+
START_MARKER = "<!-- harness-meta:start -->";
|
|
20301
|
+
END_MARKER = "<!-- harness-meta:end -->";
|
|
20302
|
+
SERIALIZER_FIELDS = [
|
|
20303
|
+
["spec", "spec", (v) => v],
|
|
20304
|
+
["plan", "plan", (v) => v],
|
|
20305
|
+
[
|
|
20306
|
+
"blocked_by",
|
|
20307
|
+
"blocked_by",
|
|
20308
|
+
(v) => Array.isArray(v) && v.length > 0 ? v : void 0
|
|
20309
|
+
],
|
|
20310
|
+
["priority", "priority", (v) => v],
|
|
20311
|
+
["milestone", "milestone", (v) => v]
|
|
20312
|
+
];
|
|
20313
|
+
GitHubIssuesTrackerAdapter = class _GitHubIssuesTrackerAdapter {
|
|
20314
|
+
http;
|
|
20315
|
+
owner;
|
|
20316
|
+
repo;
|
|
20317
|
+
cache;
|
|
20318
|
+
selectorLabel;
|
|
20319
|
+
constructor(opts) {
|
|
20320
|
+
this.http = new GitHubHttp(opts);
|
|
20321
|
+
const [owner, repo] = opts.repo.split("/");
|
|
20322
|
+
if (!owner || !repo) throw new Error(`Invalid repo "${opts.repo}", expected "owner/repo"`);
|
|
20323
|
+
this.owner = owner;
|
|
20324
|
+
this.repo = repo;
|
|
20325
|
+
this.cache = opts.etagStore ?? new ETagStore(500);
|
|
20326
|
+
this.selectorLabel = opts.selectorLabel ?? "harness-managed";
|
|
20327
|
+
}
|
|
20328
|
+
// --- Reads ---
|
|
20329
|
+
async fetchAll() {
|
|
20330
|
+
try {
|
|
20331
|
+
const cacheKey = "list:all";
|
|
20332
|
+
const cached = this.cache.get(cacheKey);
|
|
20333
|
+
const buildUrl = (page) => {
|
|
20334
|
+
const params = new URLSearchParams({
|
|
20335
|
+
state: "all",
|
|
20336
|
+
per_page: "100",
|
|
20337
|
+
page: String(page),
|
|
20338
|
+
labels: this.selectorLabel
|
|
20339
|
+
});
|
|
20340
|
+
return `${this.http.apiBase}/repos/${this.owner}/${this.repo}/issues?${params.toString()}`;
|
|
20341
|
+
};
|
|
20342
|
+
const headers = cached ? { "If-None-Match": cached.etag } : void 0;
|
|
20343
|
+
const { items, lastEtag, status } = await this.http.paginate(
|
|
20344
|
+
buildUrl,
|
|
20345
|
+
100,
|
|
20346
|
+
headers
|
|
20347
|
+
);
|
|
20348
|
+
if (status === 304 && cached) {
|
|
20349
|
+
return Ok({ features: cached.data, etag: cached.etag });
|
|
20350
|
+
}
|
|
20351
|
+
const issues = items.filter((i) => !i.pull_request);
|
|
20352
|
+
const nameIndex = /* @__PURE__ */ new Map();
|
|
20353
|
+
for (const i of issues) {
|
|
20354
|
+
nameIndex.set(i.title, buildExternalId2(this.owner, this.repo, i.number));
|
|
20355
|
+
}
|
|
20356
|
+
const features = issues.map((i) => this.mapIssue(i, nameIndex));
|
|
20357
|
+
features.sort((a, b) => a.createdAt.localeCompare(b.createdAt));
|
|
20358
|
+
if (lastEtag) this.cache.set(cacheKey, lastEtag, features);
|
|
20359
|
+
return Ok({ features, etag: lastEtag });
|
|
20360
|
+
} catch (err) {
|
|
20361
|
+
return Err(err instanceof Error ? err : new Error(String(err)));
|
|
20362
|
+
}
|
|
20363
|
+
}
|
|
20364
|
+
/**
|
|
20365
|
+
* Fetches a feature by externalId.
|
|
20366
|
+
*
|
|
20367
|
+
* Confused-deputy guard: the externalId's owner/repo MUST match the
|
|
20368
|
+
* adapter's configured owner/repo. If they differ we return Err rather
|
|
20369
|
+
* than silently issuing a request against the externalId's repo (which
|
|
20370
|
+
* the adapter is not configured to manage). The adapter's configured
|
|
20371
|
+
* owner/repo always wins.
|
|
20372
|
+
*/
|
|
20373
|
+
async fetchById(externalId) {
|
|
20374
|
+
try {
|
|
20375
|
+
const parsed = parseExternalId2(externalId);
|
|
20376
|
+
if (!parsed) return Err(new Error(`Invalid externalId: "${externalId}"`));
|
|
20377
|
+
if (parsed.owner !== this.owner || parsed.repo !== this.repo) {
|
|
20378
|
+
return Err(new Error("externalId repo does not match adapter repo"));
|
|
20379
|
+
}
|
|
20380
|
+
const cacheKey = `feature:${externalId}`;
|
|
20381
|
+
const cached = this.cache.get(cacheKey);
|
|
20382
|
+
const init = { method: "GET" };
|
|
20383
|
+
if (cached) init.extraHeaders = { "If-None-Match": cached.etag };
|
|
20384
|
+
const res = await this.http.request(
|
|
20385
|
+
`${this.http.apiBase}/repos/${parsed.owner}/${parsed.repo}/issues/${parsed.number}`,
|
|
20386
|
+
init
|
|
20387
|
+
);
|
|
20388
|
+
if (res.status === 404) return Ok(null);
|
|
20389
|
+
if (res.status === 304 && cached) {
|
|
20390
|
+
return Ok({ feature: cached.data, etag: cached.etag });
|
|
20391
|
+
}
|
|
20392
|
+
if (!res.ok) return Err(new Error(`GitHub ${res.status}: ${await res.text()}`));
|
|
20393
|
+
const data = await res.json();
|
|
20394
|
+
const etag = res.headers.get("ETag");
|
|
20395
|
+
if (data.pull_request) return Ok(null);
|
|
20396
|
+
const feature = this.mapIssue(data, /* @__PURE__ */ new Map());
|
|
20397
|
+
if (etag) this.cache.set(cacheKey, etag, feature);
|
|
20398
|
+
return Ok({ feature, etag: etag ?? "" });
|
|
20399
|
+
} catch (err) {
|
|
20400
|
+
return Err(err instanceof Error ? err : new Error(String(err)));
|
|
20401
|
+
}
|
|
20402
|
+
}
|
|
20403
|
+
async fetchByStatus(statuses) {
|
|
20404
|
+
const all = await this.fetchAll();
|
|
20405
|
+
if (!all.ok) return all;
|
|
20406
|
+
return Ok(all.value.features.filter((f) => statuses.includes(f.status)));
|
|
20407
|
+
}
|
|
20408
|
+
// --- Writes ---
|
|
20409
|
+
async create(feature) {
|
|
20410
|
+
try {
|
|
20411
|
+
const meta = buildCreateMeta(feature);
|
|
20412
|
+
const body = serializeBodyBlock(feature.summary ?? "", meta);
|
|
20413
|
+
const labels = this.buildCreateLabels(feature);
|
|
20414
|
+
const payload = { title: feature.name, body, labels };
|
|
20415
|
+
if (feature.assignee) payload.assignees = [feature.assignee.replace(/^@/, "")];
|
|
20416
|
+
const res = await this.http.request(
|
|
20417
|
+
`${this.http.apiBase}/repos/${this.owner}/${this.repo}/issues`,
|
|
20418
|
+
{ method: "POST", body: JSON.stringify(payload) }
|
|
20419
|
+
);
|
|
20420
|
+
if (!res.ok) return Err(new Error(`GitHub ${res.status}: ${await res.text()}`));
|
|
20421
|
+
const data = await res.json();
|
|
20422
|
+
this.cache.invalidatePrefix("list:");
|
|
20423
|
+
return Ok(this.mapIssue(data, /* @__PURE__ */ new Map()));
|
|
20424
|
+
} catch (err) {
|
|
20425
|
+
return Err(err instanceof Error ? err : new Error(String(err)));
|
|
20426
|
+
}
|
|
20427
|
+
}
|
|
20428
|
+
buildCreateLabels(feature) {
|
|
20429
|
+
const labels = [this.selectorLabel];
|
|
20430
|
+
if (feature.status && feature.status !== "backlog") labels.push(feature.status);
|
|
20431
|
+
return labels;
|
|
20432
|
+
}
|
|
20433
|
+
/**
|
|
20434
|
+
* Patches a feature.
|
|
20435
|
+
*
|
|
20436
|
+
* Confused-deputy guard: the externalId's owner/repo MUST match the
|
|
20437
|
+
* adapter's configured owner/repo. If they differ we return Err rather
|
|
20438
|
+
* than silently writing to the externalId's repo. The adapter's
|
|
20439
|
+
* configured owner/repo always wins.
|
|
20440
|
+
*/
|
|
20441
|
+
async update(externalId, patch, ifMatch) {
|
|
20442
|
+
const r = await this.updateInternal(externalId, patch, ifMatch);
|
|
20443
|
+
if (!r.ok) return r;
|
|
20444
|
+
return Ok(r.value.feature);
|
|
20445
|
+
}
|
|
20446
|
+
/**
|
|
20447
|
+
* Internal update that also reports whether a real PATCH was issued.
|
|
20448
|
+
* Returns wrote=false on the idempotent path so callers (claim/release/complete)
|
|
20449
|
+
* can skip best-effort history logging when no state change occurred.
|
|
20450
|
+
*
|
|
20451
|
+
* When ifMatch is supplied, updateInternal performs a refetch GET; the
|
|
20452
|
+
* refetched feature is returned as `priorFeature` so callers (release/complete)
|
|
20453
|
+
* can extract the pre-PATCH assignee for history attribution without a second GET.
|
|
20454
|
+
*/
|
|
20455
|
+
async updateInternal(externalId, patch, ifMatch) {
|
|
20456
|
+
try {
|
|
20457
|
+
const parsed = parseExternalId2(externalId);
|
|
20458
|
+
if (!parsed) return Err(new Error(`Invalid externalId: "${externalId}"`));
|
|
20459
|
+
if (parsed.owner !== this.owner || parsed.repo !== this.repo) {
|
|
20460
|
+
return Err(new Error("externalId repo does not match adapter repo"));
|
|
20461
|
+
}
|
|
20462
|
+
let priorFeature;
|
|
20463
|
+
if (ifMatch) {
|
|
20464
|
+
const cur = await this.fetchById(externalId);
|
|
20465
|
+
if (!cur.ok) return cur;
|
|
20466
|
+
if (!cur.value) return Err(new Error(`Not found: ${externalId}`));
|
|
20467
|
+
priorFeature = cur.value.feature;
|
|
20468
|
+
const cmp = refetchAndCompare(cur.value.feature, patch);
|
|
20469
|
+
if (!cmp.ok) {
|
|
20470
|
+
return Err(new ConflictError(externalId, cmp.diff, cur.value.feature.updatedAt));
|
|
20471
|
+
}
|
|
20472
|
+
if (cmp.idempotent) return Ok({ feature: cur.value.feature, wrote: false, priorFeature });
|
|
20473
|
+
}
|
|
20474
|
+
const reqBody = await this.buildIssuePatchBody(externalId, patch, priorFeature);
|
|
20475
|
+
const res = await this.http.request(
|
|
20476
|
+
`${this.http.apiBase}/repos/${parsed.owner}/${parsed.repo}/issues/${parsed.number}`,
|
|
20477
|
+
{ method: "PATCH", body: JSON.stringify(reqBody) }
|
|
20478
|
+
);
|
|
20479
|
+
if (!res.ok) return Err(new Error(`GitHub ${res.status}: ${await res.text()}`));
|
|
20480
|
+
const data = await res.json();
|
|
20481
|
+
this.cache.invalidate(`feature:${externalId}`);
|
|
20482
|
+
this.cache.invalidatePrefix("list:");
|
|
20483
|
+
return Ok({
|
|
20484
|
+
feature: this.mapIssue(data, /* @__PURE__ */ new Map()),
|
|
20485
|
+
wrote: true,
|
|
20486
|
+
...priorFeature !== void 0 ? { priorFeature } : {}
|
|
20487
|
+
});
|
|
20488
|
+
} catch (err) {
|
|
20489
|
+
return Err(err instanceof Error ? err : new Error(String(err)));
|
|
20490
|
+
}
|
|
20491
|
+
}
|
|
20492
|
+
/** Status labels recognized by mapStatus; 'backlog' and 'done' are state-driven. */
|
|
20493
|
+
static STATUS_LABELS = /* @__PURE__ */ new Set([
|
|
20494
|
+
"planned",
|
|
20495
|
+
"in-progress",
|
|
20496
|
+
"blocked",
|
|
20497
|
+
"needs-human"
|
|
20498
|
+
]);
|
|
20499
|
+
async buildIssuePatchBody(externalId, patch, priorFeature) {
|
|
20500
|
+
const out = {};
|
|
20501
|
+
if (patch.name !== void 0) out.title = patch.name;
|
|
20502
|
+
if (patch.assignee !== void 0) {
|
|
20503
|
+
out.assignees = patch.assignee ? [patch.assignee.replace(/^@/, "")] : [];
|
|
20504
|
+
}
|
|
20505
|
+
if (patch.status !== void 0) {
|
|
20506
|
+
if (patch.status === "done") out.state = "closed";
|
|
20507
|
+
else out.state = "open";
|
|
20508
|
+
const labelsR = await this.fetchRawLabels(externalId);
|
|
20509
|
+
if (labelsR.ok) {
|
|
20510
|
+
const filtered = labelsR.value.filter(
|
|
20511
|
+
(l) => !_GitHubIssuesTrackerAdapter.STATUS_LABELS.has(l)
|
|
20512
|
+
);
|
|
20513
|
+
if (patch.status !== "backlog" && patch.status !== "done" && _GitHubIssuesTrackerAdapter.STATUS_LABELS.has(patch.status)) {
|
|
20514
|
+
filtered.push(patch.status);
|
|
20515
|
+
}
|
|
20516
|
+
out.labels = filtered;
|
|
20517
|
+
} else {
|
|
20518
|
+
console.warn(
|
|
20519
|
+
`harness-tracker: skipping label sync on ${externalId} due to GET error: ${labelsR.error.message}`
|
|
20520
|
+
);
|
|
20521
|
+
}
|
|
20522
|
+
}
|
|
20523
|
+
const bodyTouches = [
|
|
20524
|
+
"summary",
|
|
20525
|
+
"spec",
|
|
20526
|
+
"plans",
|
|
20527
|
+
"blockedBy",
|
|
20528
|
+
"priority",
|
|
20529
|
+
"milestone"
|
|
20530
|
+
];
|
|
20531
|
+
if (bodyTouches.some((k) => patch[k] !== void 0)) {
|
|
20532
|
+
let current;
|
|
20533
|
+
if (priorFeature) {
|
|
20534
|
+
current = priorFeature;
|
|
20535
|
+
} else {
|
|
20536
|
+
const cur = await this.fetchById(externalId);
|
|
20537
|
+
if (!cur.ok || !cur.value) throw new Error("Cannot rebuild body without current state");
|
|
20538
|
+
current = cur.value.feature;
|
|
20539
|
+
}
|
|
20540
|
+
const meta = metaFromFeatureFields({
|
|
20541
|
+
spec: patch.spec !== void 0 ? patch.spec : current.spec,
|
|
20542
|
+
plans: patch.plans !== void 0 ? patch.plans : current.plans,
|
|
20543
|
+
blockedBy: patch.blockedBy !== void 0 ? patch.blockedBy : current.blockedBy,
|
|
20544
|
+
priority: patch.priority !== void 0 ? patch.priority : current.priority,
|
|
20545
|
+
milestone: patch.milestone !== void 0 ? patch.milestone : current.milestone
|
|
20546
|
+
});
|
|
20547
|
+
const summaryText = patch.summary !== void 0 ? patch.summary : current.summary;
|
|
20548
|
+
out.body = serializeBodyBlock(summaryText, meta);
|
|
20549
|
+
}
|
|
20550
|
+
return out;
|
|
20551
|
+
}
|
|
20552
|
+
/**
|
|
20553
|
+
* Lightweight GET that returns the raw label name list.
|
|
20554
|
+
*
|
|
20555
|
+
* P2-RR2-IMP-1: returns Result so callers can distinguish a real Err (network
|
|
20556
|
+
* failure / non-200 response) from an empty label set. The previous shape
|
|
20557
|
+
* silently swallowed errors and returned [], which combined with the
|
|
20558
|
+
* unconditional `out.labels = filtered` assignment in buildIssuePatchBody
|
|
20559
|
+
* caused PATCH requests to wipe ALL labels (including the selectorLabel and
|
|
20560
|
+
* user-added labels) on a transient GET blip.
|
|
20561
|
+
*/
|
|
20562
|
+
async fetchRawLabels(externalId) {
|
|
20563
|
+
try {
|
|
20564
|
+
const parsed = parseExternalId2(externalId);
|
|
20565
|
+
if (!parsed) return Err(new Error(`Invalid externalId: "${externalId}"`));
|
|
20566
|
+
const res = await this.http.request(
|
|
20567
|
+
`${this.http.apiBase}/repos/${parsed.owner}/${parsed.repo}/issues/${parsed.number}`,
|
|
20568
|
+
{ method: "GET" }
|
|
20569
|
+
);
|
|
20570
|
+
if (!res.ok) return Err(new Error(`GitHub ${res.status}: ${await res.text()}`));
|
|
20571
|
+
const data = await res.json();
|
|
20572
|
+
return Ok(data.labels.map((l) => l.name));
|
|
20573
|
+
} catch (err) {
|
|
20574
|
+
return Err(err instanceof Error ? err : new Error(String(err)));
|
|
20575
|
+
}
|
|
20576
|
+
}
|
|
20577
|
+
async claim(externalId, assignee, ifMatch) {
|
|
20578
|
+
const r = await this.updateInternal(externalId, { assignee, status: "in-progress" }, ifMatch);
|
|
20579
|
+
if (!r.ok) return r;
|
|
20580
|
+
if (r.value.wrote) await this.logEvent(externalId, "claimed", assignee);
|
|
20581
|
+
return Ok(r.value.feature);
|
|
20582
|
+
}
|
|
20583
|
+
async release(externalId, ifMatch) {
|
|
20584
|
+
const priorAssignee = await this.fetchPriorAssigneeIfNeeded(externalId, ifMatch);
|
|
20585
|
+
const r = await this.updateInternal(externalId, { assignee: null, status: "backlog" }, ifMatch);
|
|
20586
|
+
if (!r.ok) return r;
|
|
20587
|
+
if (r.value.wrote) {
|
|
20588
|
+
const actor = r.value.priorFeature?.assignee ?? priorAssignee ?? null;
|
|
20589
|
+
await this.logEvent(externalId, "released", actor ?? "unknown");
|
|
20590
|
+
}
|
|
20591
|
+
return Ok(r.value.feature);
|
|
20592
|
+
}
|
|
20593
|
+
async complete(externalId, ifMatch) {
|
|
20594
|
+
const priorAssignee = await this.fetchPriorAssigneeIfNeeded(externalId, ifMatch);
|
|
20595
|
+
const r = await this.updateInternal(externalId, { status: "done" }, ifMatch);
|
|
20596
|
+
if (!r.ok) return r;
|
|
20597
|
+
if (r.value.wrote) {
|
|
20598
|
+
const actor = r.value.priorFeature?.assignee ?? priorAssignee ?? null;
|
|
20599
|
+
await this.logEvent(externalId, "completed", actor ?? "unknown");
|
|
20600
|
+
}
|
|
20601
|
+
return Ok(r.value.feature);
|
|
20602
|
+
}
|
|
20603
|
+
/**
|
|
20604
|
+
* Best-effort GET to capture the assignee BEFORE a PATCH that may clear it.
|
|
20605
|
+
* Skipped when ifMatch is supplied because updateInternal will refetch and
|
|
20606
|
+
* thread the prior feature through.
|
|
20607
|
+
*/
|
|
20608
|
+
async fetchPriorAssigneeIfNeeded(externalId, ifMatch) {
|
|
20609
|
+
if (ifMatch) return null;
|
|
20610
|
+
const cur = await this.fetchById(externalId);
|
|
20611
|
+
if (cur.ok && cur.value) return cur.value.feature.assignee;
|
|
20612
|
+
return null;
|
|
20613
|
+
}
|
|
20614
|
+
/**
|
|
20615
|
+
* Trust model for history events.
|
|
20616
|
+
*
|
|
20617
|
+
* `HISTORY_PREFIX` (`<!-- harness-history -->`) marks issue comments as
|
|
20618
|
+
* authentic harness events. fetchHistory reads only comments that begin
|
|
20619
|
+
* with this prefix; appendHistory writes the prefix unconditionally.
|
|
20620
|
+
*
|
|
20621
|
+
* Trust is per-repo: only repository contributors authorized to post
|
|
20622
|
+
* comments on a given issue can synthesize harness history events.
|
|
20623
|
+
* GitHub's per-repo permissions are the authorization boundary; the
|
|
20624
|
+
* prefix itself is not a cryptographic credential — a contributor with
|
|
20625
|
+
* comment access can forge events by hand-writing the prefix.
|
|
20626
|
+
*
|
|
20627
|
+
* HMAC-signing of event payloads is a future hardening (post-GA) for
|
|
20628
|
+
* deployments that need cross-actor tamper-evidence within the same
|
|
20629
|
+
* repo. The current shape is intentional: optimized for transparency
|
|
20630
|
+
* (events are human-readable, no key distribution required).
|
|
20631
|
+
*/
|
|
20632
|
+
static HISTORY_PREFIX = "<!-- harness-history -->";
|
|
20633
|
+
async appendHistory(externalId, event) {
|
|
20634
|
+
try {
|
|
20635
|
+
const parsed = parseExternalId2(externalId);
|
|
20636
|
+
if (!parsed) return Err(new Error(`Invalid externalId: "${externalId}"`));
|
|
20637
|
+
const body = `${_GitHubIssuesTrackerAdapter.HISTORY_PREFIX}
|
|
20638
|
+
${JSON.stringify(event)}`;
|
|
20639
|
+
const res = await this.http.request(
|
|
20640
|
+
`${this.http.apiBase}/repos/${parsed.owner}/${parsed.repo}/issues/${parsed.number}/comments`,
|
|
20641
|
+
{ method: "POST", body: JSON.stringify({ body }) }
|
|
20642
|
+
);
|
|
20643
|
+
if (!res.ok) return Err(new Error(`GitHub ${res.status}: ${await res.text()}`));
|
|
20644
|
+
return Ok(void 0);
|
|
20645
|
+
} catch (err) {
|
|
20646
|
+
return Err(err instanceof Error ? err : new Error(String(err)));
|
|
20647
|
+
}
|
|
20648
|
+
}
|
|
20649
|
+
async fetchHistory(externalId, limit) {
|
|
20650
|
+
try {
|
|
20651
|
+
const parsed = parseExternalId2(externalId);
|
|
20652
|
+
if (!parsed) return Err(new Error(`Invalid externalId: "${externalId}"`));
|
|
20653
|
+
const buildUrl = (page) => `${this.http.apiBase}/repos/${parsed.owner}/${parsed.repo}/issues/${parsed.number}/comments?per_page=100&page=${page}`;
|
|
20654
|
+
const { items } = await this.http.paginate(buildUrl);
|
|
20655
|
+
const events = [];
|
|
20656
|
+
for (const c of items) {
|
|
20657
|
+
if (!c.body.startsWith(_GitHubIssuesTrackerAdapter.HISTORY_PREFIX)) continue;
|
|
20658
|
+
const json = c.body.slice(_GitHubIssuesTrackerAdapter.HISTORY_PREFIX.length).trim();
|
|
20659
|
+
try {
|
|
20660
|
+
events.push(JSON.parse(json));
|
|
20661
|
+
} catch (e) {
|
|
20662
|
+
console.warn(`harness-history: malformed JSON in ${externalId}: ${e.message}`);
|
|
20663
|
+
}
|
|
20664
|
+
}
|
|
20665
|
+
events.sort((a, b) => a.at.localeCompare(b.at));
|
|
20666
|
+
return Ok(limit ? events.slice(-limit) : events);
|
|
20667
|
+
} catch (err) {
|
|
20668
|
+
return Err(err instanceof Error ? err : new Error(String(err)));
|
|
20669
|
+
}
|
|
20670
|
+
}
|
|
20671
|
+
async logEvent(externalId, type, actor) {
|
|
20672
|
+
const event = {
|
|
20673
|
+
type,
|
|
20674
|
+
actor,
|
|
20675
|
+
at: (/* @__PURE__ */ new Date()).toISOString()
|
|
20676
|
+
};
|
|
20677
|
+
const r = await this.appendHistory(externalId, event);
|
|
20678
|
+
if (!r.ok) {
|
|
20679
|
+
console.warn(`harness-history: append failed for ${externalId}: ${r.error.message}`);
|
|
20680
|
+
}
|
|
20681
|
+
}
|
|
20682
|
+
// --- Helpers ---
|
|
20683
|
+
// The nameIndex parameter is retained for backwards-compatibility with the
|
|
20684
|
+
// fetchAll signature but is no longer consulted: blockedBy holds feature
|
|
20685
|
+
// names verbatim from the body-meta block (see client.ts TrackedFeature.blockedBy).
|
|
20686
|
+
mapIssue(issue, _nameIndex) {
|
|
20687
|
+
const { summary, meta } = parseBodyBlock(issue.body ?? "");
|
|
20688
|
+
const status = this.mapStatus(issue, meta);
|
|
20689
|
+
return {
|
|
20690
|
+
externalId: buildExternalId2(this.owner, this.repo, issue.number),
|
|
20691
|
+
name: issue.title,
|
|
20692
|
+
status,
|
|
20693
|
+
summary,
|
|
20694
|
+
spec: meta.spec ?? null,
|
|
20695
|
+
plans: meta.plan ? [meta.plan] : [],
|
|
20696
|
+
blockedBy: meta.blocked_by ?? [],
|
|
20697
|
+
assignee: issue.assignees[0]?.login ? `@${issue.assignees[0].login}` : null,
|
|
20698
|
+
priority: meta.priority ?? null,
|
|
20699
|
+
milestone: issue.milestone?.title ?? meta.milestone ?? null,
|
|
20700
|
+
createdAt: issue.created_at,
|
|
20701
|
+
updatedAt: issue.updated_at ?? null
|
|
20702
|
+
};
|
|
20703
|
+
}
|
|
20704
|
+
/**
|
|
20705
|
+
* Status precedence (highest wins):
|
|
20706
|
+
* 1. issue.state === 'closed' → 'done'
|
|
20707
|
+
* 2. 'blocked' label → 'blocked'
|
|
20708
|
+
* 3. 'needs-human' label → 'needs-human'
|
|
20709
|
+
* 4. 'in-progress' label OR any assignee → 'in-progress'
|
|
20710
|
+
* 5. 'planned' label → 'planned'
|
|
20711
|
+
* 6. otherwise → 'backlog'
|
|
20712
|
+
*
|
|
20713
|
+
* Note step 4: an assignment overrides a 'planned' label. This is
|
|
20714
|
+
* intentional — a feature that someone has picked up is "in-progress"
|
|
20715
|
+
* regardless of whether the planning label was cleared first. The reverse
|
|
20716
|
+
* (planned label + assignee) is the common case where the orchestrator
|
|
20717
|
+
* (or a human) starts work without scrubbing the planning label.
|
|
20718
|
+
*/
|
|
20719
|
+
mapStatus(issue, _meta) {
|
|
20720
|
+
if (issue.state === "closed") return "done";
|
|
20721
|
+
const labels = issue.labels.map((l) => l.name);
|
|
20722
|
+
if (labels.includes("blocked")) return "blocked";
|
|
20723
|
+
if (labels.includes("needs-human")) return "needs-human";
|
|
20724
|
+
if (labels.includes("in-progress") || issue.assignees.length > 0) return "in-progress";
|
|
20725
|
+
if (labels.includes("planned")) return "planned";
|
|
20726
|
+
return "backlog";
|
|
20727
|
+
}
|
|
20728
|
+
};
|
|
20729
|
+
migrate_exports = {};
|
|
20730
|
+
__export2(migrate_exports, {
|
|
20731
|
+
bodyMetaMatches: () => bodyMetaMatches,
|
|
20732
|
+
buildHistoryCommentBody: () => buildHistoryCommentBody,
|
|
20733
|
+
buildMigrationPlan: () => buildMigrationPlan,
|
|
20734
|
+
hashHistoryEvent: () => hashHistoryEvent,
|
|
20735
|
+
parseHashFromCommentBody: () => parseHashFromCommentBody,
|
|
20736
|
+
runMigrationPlan: () => runMigrationPlan
|
|
20737
|
+
});
|
|
20738
|
+
HASH_RE = /<!--\s*harness-history\s+hash:([0-9a-f]{8})\s*-->/i;
|
|
19523
20739
|
InteractionTypeSchema = z12.enum(["question", "confirmation", "transition"]);
|
|
19524
20740
|
QuestionSchema = z12.object({
|
|
19525
20741
|
text: z12.string(),
|
|
@@ -19552,10 +20768,10 @@ var init_dist3 = __esm({
|
|
|
19552
20768
|
}
|
|
19553
20769
|
rootDir;
|
|
19554
20770
|
async scan() {
|
|
19555
|
-
let projectName =
|
|
20771
|
+
let projectName = path34.basename(this.rootDir);
|
|
19556
20772
|
try {
|
|
19557
|
-
const pkgPath =
|
|
19558
|
-
const pkgRaw = await
|
|
20773
|
+
const pkgPath = path34.join(this.rootDir, "package.json");
|
|
20774
|
+
const pkgRaw = await fs34.readFile(pkgPath, "utf-8");
|
|
19559
20775
|
const pkg = JSON.parse(pkgRaw);
|
|
19560
20776
|
if (pkg.name) projectName = pkg.name;
|
|
19561
20777
|
} catch {
|
|
@@ -19668,8 +20884,8 @@ console.log('Blueprint player initialized.');
|
|
|
19668
20884
|
styles: STYLES,
|
|
19669
20885
|
scripts: SCRIPTS
|
|
19670
20886
|
});
|
|
19671
|
-
await
|
|
19672
|
-
await
|
|
20887
|
+
await fs35.mkdir(options.outputDir, { recursive: true });
|
|
20888
|
+
await fs35.writeFile(path35.join(options.outputDir, "index.html"), html);
|
|
19673
20889
|
}
|
|
19674
20890
|
};
|
|
19675
20891
|
TOKENS_PER_MILLION = 1e6;
|
|
@@ -20102,6 +21318,8 @@ export {
|
|
|
20102
21318
|
validateAgentsMap,
|
|
20103
21319
|
runFallbackRules,
|
|
20104
21320
|
validateAgentConfigs,
|
|
21321
|
+
getRoadmapMode,
|
|
21322
|
+
validateRoadmapMode,
|
|
20105
21323
|
checkDocCoverage,
|
|
20106
21324
|
validateKnowledgeMap,
|
|
20107
21325
|
generateAgentsMap,
|
|
@@ -20391,8 +21609,17 @@ export {
|
|
|
20391
21609
|
syncToExternal,
|
|
20392
21610
|
syncFromExternal,
|
|
20393
21611
|
fullSync,
|
|
21612
|
+
scoreRoadmapCandidatesFileLess,
|
|
20394
21613
|
scoreRoadmapCandidates,
|
|
20395
21614
|
assignFeature,
|
|
21615
|
+
scoreRoadmapCandidatesForMode,
|
|
21616
|
+
ConflictError,
|
|
21617
|
+
ETagStore,
|
|
21618
|
+
createTrackerClient,
|
|
21619
|
+
makeTrackerConflictBody,
|
|
21620
|
+
loadProjectRoadmapMode,
|
|
21621
|
+
loadTrackerClientConfigFromProject,
|
|
21622
|
+
migrate_exports,
|
|
20396
21623
|
InteractionTypeSchema,
|
|
20397
21624
|
QuestionSchema,
|
|
20398
21625
|
ConfirmationSchema,
|