halfcycle 0.4.14 → 0.4.15
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/.claude-plugin/plugin.json +1 -1
- package/bin/bin.bundle.mjs +2525 -541
- package/bin/pre-tool-use-filter.sh +41 -5
- package/commands/agent-teams-orchestration.md +1 -1
- package/commands/consistency-check.md +1 -1
- package/commands/context-update.md +1 -1
- package/commands/halfcycle-design.md +1 -1
- package/commands/halfcycle-setup.md +1 -1
- package/commands/stack-assembly-worker.md +1 -1
- package/commands/task-execution.md +1 -1
- package/commands/task-graph.md +1 -1
- package/commands/write-spec.md +1 -1
- package/dist/bin.js +0 -0
- package/dist/build-record/render-v2.d.ts.map +1 -1
- package/dist/claude-plan.d.ts +24 -5
- package/dist/claude-plan.d.ts.map +1 -1
- package/dist/cli-contract.d.ts +1 -1
- package/dist/cli-contract.d.ts.map +1 -1
- package/dist/cli.js +2242 -565
- package/dist/cli.js.map +3 -3
- package/dist/close-phase.d.ts.map +1 -1
- package/dist/doctor.d.ts +34 -0
- package/dist/doctor.d.ts.map +1 -0
- package/dist/folder-refusals.d.ts +27 -0
- package/dist/folder-refusals.d.ts.map +1 -0
- package/dist/identity.d.ts +31 -1
- package/dist/identity.d.ts.map +1 -1
- package/dist/index.js +1276 -386
- package/dist/index.js.map +3 -3
- package/dist/install-plan.d.ts +112 -0
- package/dist/install-plan.d.ts.map +1 -0
- package/dist/install.d.ts +535 -1
- package/dist/install.d.ts.map +1 -1
- package/dist/plan.d.ts +16 -3
- package/dist/plan.d.ts.map +1 -1
- package/dist/pre-create-checks.d.ts +28 -0
- package/dist/pre-create-checks.d.ts.map +1 -1
- package/dist/preview.d.ts +40 -0
- package/dist/preview.d.ts.map +1 -0
- package/dist/restore.d.ts +58 -0
- package/dist/restore.d.ts.map +1 -0
- package/dist/review.d.ts.map +1 -1
- package/dist/scan.d.ts +11 -0
- package/dist/scan.d.ts.map +1 -1
- package/dist/setup/discovery.d.ts.map +1 -1
- package/dist/setup/rows.d.ts +10 -6
- package/dist/setup/rows.d.ts.map +1 -1
- package/package.json +12 -12
package/dist/index.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
// dist/install.js
|
|
2
2
|
import { execFileSync as execFileSync2 } from "node:child_process";
|
|
3
3
|
import { createHash as createHash2 } from "node:crypto";
|
|
4
|
-
import { existsSync as
|
|
4
|
+
import { existsSync as existsSync4, lstatSync, mkdirSync as mkdirSync4, readFileSync as readFileSync5, rmSync as rmSync2, writeFileSync as writeFileSync4 } from "node:fs";
|
|
5
5
|
import { homedir as homedir2 } from "node:os";
|
|
6
|
-
import { dirname as dirname3, isAbsolute as isAbsolute2, join as
|
|
6
|
+
import { dirname as dirname3, isAbsolute as isAbsolute2, join as join6, posix as posix2, relative, resolve } from "node:path";
|
|
7
7
|
import { fileURLToPath } from "node:url";
|
|
8
8
|
|
|
9
9
|
// ../core/dist/guard-record.js
|
|
@@ -122,9 +122,10 @@ var SESSION_START_MARKER_REL = ".claude/hooks/session-start-marker.sh";
|
|
|
122
122
|
var GUARD_RUNNER_REL = ".claude/hooks/guard-runner.sh";
|
|
123
123
|
var USER_PROMPT_REMINDER_REL = ".claude/hooks/user-prompt-reminder.sh";
|
|
124
124
|
var USER_PROMPT_SCOPE_REL = ".claude/hooks/user-prompt-scope.sh";
|
|
125
|
+
var USER_PROMPT_FINDINGS_REL = ".claude/hooks/user-prompt-findings.sh";
|
|
125
126
|
var SESSION_END_MARKER_REL = ".claude/hooks/session-end-marker.sh";
|
|
126
127
|
var WRITE_CAPABLE_TOOLS = ["Edit", "MultiEdit", "NotebookEdit", "Write"];
|
|
127
|
-
var PRE_TOOL_USE_MATCHER = "^(Bash|Read|Grep|Write|Edit|MultiEdit|NotebookEdit|Agent)$";
|
|
128
|
+
var PRE_TOOL_USE_MATCHER = "^(Bash|Read|Grep|Write|Edit|MultiEdit|NotebookEdit|Agent|PowerShell)$";
|
|
128
129
|
function hookCommand(script, verb) {
|
|
129
130
|
const invocation = `bash "$CLAUDE_PROJECT_DIR"/${script}`;
|
|
130
131
|
return verb === void 0 ? invocation : `${invocation} ${verb}`;
|
|
@@ -158,6 +159,13 @@ var HALFCYCLE_HOOK_WIRING = [
|
|
|
158
159
|
command: hookCommand(USER_PROMPT_SCOPE_REL),
|
|
159
160
|
script: USER_PROMPT_SCOPE_REL
|
|
160
161
|
},
|
|
162
|
+
{
|
|
163
|
+
event: "UserPromptSubmit",
|
|
164
|
+
matcher: null,
|
|
165
|
+
timeout: 10,
|
|
166
|
+
command: hookCommand(USER_PROMPT_FINDINGS_REL),
|
|
167
|
+
script: USER_PROMPT_FINDINGS_REL
|
|
168
|
+
},
|
|
161
169
|
{
|
|
162
170
|
event: "PreToolUse",
|
|
163
171
|
matcher: PRE_TOOL_USE_MATCHER,
|
|
@@ -1243,8 +1251,9 @@ var SCOPE_PART_STATUSES = ["approved", "waiting"];
|
|
|
1243
1251
|
var scopePartStatusSchema = z10.enum(SCOPE_PART_STATUSES);
|
|
1244
1252
|
var SCOPE_ACTS = ["approve", "send-back"];
|
|
1245
1253
|
var scopeActSchema = z10.enum(SCOPE_ACTS);
|
|
1246
|
-
var SCOPE_GIVEN_IN = ["console", "chat"];
|
|
1254
|
+
var SCOPE_GIVEN_IN = ["console", "chat", "autonomy"];
|
|
1247
1255
|
var scopeGivenInSchema = z10.enum(SCOPE_GIVEN_IN);
|
|
1256
|
+
var APPROVED_IN_ADVANCE_SENTENCE = "approved in advance by your autonomy setting";
|
|
1248
1257
|
var scopeNoteSchema = descriptionText(SCOPE_NOTE_MAX);
|
|
1249
1258
|
var scopeNoteWireSchema = z10.string().max(SCOPE_NOTE_MAX);
|
|
1250
1259
|
var SCOPE_PART_VERDICTS = ["ok", "change"];
|
|
@@ -1275,7 +1284,8 @@ var phasePlanRecordedSchema = z10.object({
|
|
|
1275
1284
|
typedApprovalOffered: z10.boolean(),
|
|
1276
1285
|
reportedAt: utcIso8601,
|
|
1277
1286
|
carriedFrom: z10.array(carriedFromEntrySchema).min(1).max(PHASE_PLAN_CHANGES_MAX).optional(),
|
|
1278
|
-
movedUp: planMovedUpSchema.optional()
|
|
1287
|
+
movedUp: planMovedUpSchema.optional(),
|
|
1288
|
+
givenIn: scopeGivenInSchema.optional()
|
|
1279
1289
|
}).strict();
|
|
1280
1290
|
var phasePlanRefusedSchema = z10.object({
|
|
1281
1291
|
outcome: z10.literal("refused"),
|
|
@@ -2282,6 +2292,15 @@ var REVIEW_PART_CHANGES = Object.freeze([...PHASE_PLAN_CHANGE_KINDS, "not-in-pla
|
|
|
2282
2292
|
var reviewPartChangeSchema = z17.enum(REVIEW_PART_CHANGES);
|
|
2283
2293
|
var REVIEW_DECISIONS = Object.freeze(["accepted", "carried", "dropped"]);
|
|
2284
2294
|
var reviewDecisionSchema = z17.enum(REVIEW_DECISIONS);
|
|
2295
|
+
var reviewGivenInSchema = scopeGivenInSchema.extract(["autonomy"]);
|
|
2296
|
+
var DECIDED_BY_AUTONOMY_SENTENCE = "decided by your autonomy setting";
|
|
2297
|
+
function checkReviewGivenIn(v, ctx) {
|
|
2298
|
+
if (v.givenIn === void 0)
|
|
2299
|
+
return;
|
|
2300
|
+
if (v.decision !== "accepted" && v.decision !== "carried") {
|
|
2301
|
+
ctx.addIssue({ code: "custom", path: ["givenIn"], message: "is present only on a part accepted or carried" });
|
|
2302
|
+
}
|
|
2303
|
+
}
|
|
2285
2304
|
function actOf(decision) {
|
|
2286
2305
|
if (decision === "accepted")
|
|
2287
2306
|
return "accept";
|
|
@@ -2347,6 +2366,25 @@ var reviewRefusedSchema = z17.object({
|
|
|
2347
2366
|
function bareReviewOutcome(outcome) {
|
|
2348
2367
|
return z17.object({ outcome: z17.literal(outcome) }).strict();
|
|
2349
2368
|
}
|
|
2369
|
+
var reviewClosedByAutonomySchema = z17.object({
|
|
2370
|
+
outcome: z17.literal("closed-by-autonomy"),
|
|
2371
|
+
accepted: z17.number().int().min(0).max(REVIEW_PARTS_MAX),
|
|
2372
|
+
carried: z17.number().int().min(0).max(REVIEW_PARTS_MAX)
|
|
2373
|
+
}).strict().refine((v) => v.accepted + v.carried <= REVIEW_PARTS_MAX, {
|
|
2374
|
+
message: "cannot exceed the parts a round holds",
|
|
2375
|
+
path: ["carried"]
|
|
2376
|
+
});
|
|
2377
|
+
var AUTONOMY_WAIT_REASONS = ["unchecked-claims", "built-without-approval"];
|
|
2378
|
+
var autonomyWaitReasonSchema = z17.enum(AUTONOMY_WAIT_REASONS);
|
|
2379
|
+
var reviewAutonomyWaitsSchema = z17.object({
|
|
2380
|
+
outcome: z17.literal("autonomy-waits"),
|
|
2381
|
+
reason: autonomyWaitReasonSchema,
|
|
2382
|
+
count: z17.number().int().min(1),
|
|
2383
|
+
parts: z17.array(z17.object({ part: systemPartIdSchema, name: systemPartSchema.shape.name }).strict()).min(1).max(REVIEW_PARTS_MAX)
|
|
2384
|
+
}).strict().refine((v) => v.reason !== "built-without-approval" || v.count === v.parts.length, {
|
|
2385
|
+
message: "counts the parts it names",
|
|
2386
|
+
path: ["count"]
|
|
2387
|
+
});
|
|
2350
2388
|
var openReviewResultSchema = z17.discriminatedUnion("outcome", [
|
|
2351
2389
|
reviewOpenedSchema,
|
|
2352
2390
|
reviewTasksOpenSchema,
|
|
@@ -2355,7 +2393,9 @@ var openReviewResultSchema = z17.discriminatedUnion("outcome", [
|
|
|
2355
2393
|
reviewOutstandingSchema,
|
|
2356
2394
|
reviewCleanOverFindingsSchema,
|
|
2357
2395
|
reviewRefusedSchema,
|
|
2358
|
-
bareReviewOutcome("unavailable")
|
|
2396
|
+
bareReviewOutcome("unavailable"),
|
|
2397
|
+
reviewClosedByAutonomySchema,
|
|
2398
|
+
reviewAutonomyWaitsSchema
|
|
2359
2399
|
]);
|
|
2360
2400
|
var reviewPhaseNotOpenSchema = z17.object({
|
|
2361
2401
|
statusCode: z17.literal(409),
|
|
@@ -2469,6 +2509,7 @@ var reviewViewPartSchema = z17.object({
|
|
|
2469
2509
|
decidable: z17.boolean(),
|
|
2470
2510
|
waiting: z17.boolean(),
|
|
2471
2511
|
decision: reviewDecisionSchema.optional(),
|
|
2512
|
+
givenIn: reviewGivenInSchema.optional(),
|
|
2472
2513
|
reason: reviewNoteSchema.optional(),
|
|
2473
2514
|
carriedTo: reviewCarriedToSchema.optional(),
|
|
2474
2515
|
sentBack: z17.array(reviewSentBackSchema),
|
|
@@ -2496,6 +2537,7 @@ var reviewViewPartSchema = z17.object({
|
|
|
2496
2537
|
if (v.change === "not-in-plan" && !v.built) {
|
|
2497
2538
|
ctx.addIssue({ code: "custom", path: ["built"], message: "is always true for a part outside the plan" });
|
|
2498
2539
|
}
|
|
2540
|
+
checkReviewGivenIn(v, ctx);
|
|
2499
2541
|
});
|
|
2500
2542
|
var reviewPhaseNumberSchema = z17.number().int().min(1).nullable();
|
|
2501
2543
|
var phaseReviewNoneSchema = z17.object({
|
|
@@ -2590,6 +2632,7 @@ var recordReviewPartSchema = z18.object({
|
|
|
2590
2632
|
built: z18.boolean(),
|
|
2591
2633
|
builtWithoutApproval: z18.boolean(),
|
|
2592
2634
|
decision: reviewDecisionSchema,
|
|
2635
|
+
givenIn: reviewGivenInSchema.optional(),
|
|
2593
2636
|
reason: reviewNoteSchema.optional(),
|
|
2594
2637
|
carriedTo: reviewCarriedToSchema.optional(),
|
|
2595
2638
|
sentBack: z18.array(reviewSentBackSchema),
|
|
@@ -2603,6 +2646,7 @@ var recordReviewPartSchema = z18.object({
|
|
|
2603
2646
|
if (v.carriedTo !== void 0 !== (v.decision === "carried")) {
|
|
2604
2647
|
ctx.addIssue({ code: "custom", path: ["carriedTo"], message: "is present exactly on a carry" });
|
|
2605
2648
|
}
|
|
2649
|
+
checkReviewGivenIn(v, ctx);
|
|
2606
2650
|
});
|
|
2607
2651
|
var SPINE_ORDER = boardGateNameSchema.options;
|
|
2608
2652
|
var recordReviewGatesSchema = z18.array(recordReviewGateSchema).superRefine((gates, ctx) => {
|
|
@@ -2898,6 +2942,251 @@ var phaseRecordResponseSchema = z22.discriminatedUnion("outcome", [
|
|
|
2898
2942
|
phaseRecordRefusedSchema
|
|
2899
2943
|
]);
|
|
2900
2944
|
|
|
2945
|
+
// ../events/dist/owner-waits.js
|
|
2946
|
+
import { z as z23 } from "zod";
|
|
2947
|
+
var OWNER_WAIT_ITEMS = Object.freeze([
|
|
2948
|
+
Object.freeze({ id: "delete-users-data", words: "Deleting or replacing data people already have." }),
|
|
2949
|
+
Object.freeze({
|
|
2950
|
+
id: "change-promises",
|
|
2951
|
+
words: "Changing what the product promises its users, such as pricing, terms or privacy."
|
|
2952
|
+
}),
|
|
2953
|
+
Object.freeze({ id: "spend-or-sign-up", words: "Anything that costs money or signs up for a service." }),
|
|
2954
|
+
Object.freeze({ id: "publish-or-release", words: "Publishing or releasing to users." }),
|
|
2955
|
+
Object.freeze({ id: "reshape-shared-data", words: "Dropping or reshaping stored data that other parts read." }),
|
|
2956
|
+
Object.freeze({ id: "change-standing-rule", words: "Changing a standing rule." })
|
|
2957
|
+
]);
|
|
2958
|
+
var OWNER_WAIT_ITEM_IDS = Object.freeze(OWNER_WAIT_ITEMS.map((item) => item.id));
|
|
2959
|
+
var ownerWaitItemIdSchema = z23.enum(OWNER_WAIT_ITEM_IDS);
|
|
2960
|
+
var OWNER_WAIT_FIXED_COUNT = OWNER_WAIT_ITEMS.length;
|
|
2961
|
+
var OWNER_WAIT_ADDITIONS_MAX = 20;
|
|
2962
|
+
var OWNER_WAIT_TEXT_MAX = 240;
|
|
2963
|
+
var ownerWaitItemRefSchema = z23.union([
|
|
2964
|
+
z23.number().int().min(1),
|
|
2965
|
+
z23.string().uuid({ message: "must be a number from 1 to 6, or the id shown beside an added item" })
|
|
2966
|
+
]);
|
|
2967
|
+
var ownerWaitText = descriptionText(OWNER_WAIT_TEXT_MAX);
|
|
2968
|
+
var ownerWaitAddRequestSchema = z23.object({
|
|
2969
|
+
engagementId: z23.string().min(1),
|
|
2970
|
+
text: ownerWaitText,
|
|
2971
|
+
reason: ownerWaitText
|
|
2972
|
+
}).strict();
|
|
2973
|
+
var ownerWaitAddResultSchema = z23.discriminatedUnion("outcome", [
|
|
2974
|
+
z23.object({ outcome: z23.literal("added"), id: z23.string().uuid(), addedAt: utcIso8601 }).strict(),
|
|
2975
|
+
z23.object({ outcome: z23.literal("refused"), reason: z23.literal("too-many") }).strict()
|
|
2976
|
+
]);
|
|
2977
|
+
var ownerWaitUndoableSchema = z23.enum(["yes", "no"]);
|
|
2978
|
+
var ownerWaitDeclareRequestSchema = z23.object({
|
|
2979
|
+
engagementId: z23.string().min(1),
|
|
2980
|
+
item: ownerWaitItemRefSchema,
|
|
2981
|
+
question: ownerWaitText,
|
|
2982
|
+
recommendation: ownerWaitText,
|
|
2983
|
+
task: buildTaskIdSchema.optional(),
|
|
2984
|
+
by: agentNameSchema.optional(),
|
|
2985
|
+
undoable: ownerWaitUndoableSchema.optional(),
|
|
2986
|
+
how: statementText(STATEMENT_MAX).optional()
|
|
2987
|
+
}).strict().refine((r) => r.undoable === "yes" === (r.how !== void 0), {
|
|
2988
|
+
message: "says how to undo it exactly when it can be undone",
|
|
2989
|
+
path: ["how"]
|
|
2990
|
+
});
|
|
2991
|
+
var OWNER_WAIT_DECLARE_REFUSALS = ["no-open-phase", "unknown-item", "say-if-undoable"];
|
|
2992
|
+
var ownerWaitDeclareResultSchema = z23.discriminatedUnion("outcome", [
|
|
2993
|
+
z23.object({ outcome: z23.literal("recorded"), id: z23.string().uuid(), say: z23.string().min(1) }).strict(),
|
|
2994
|
+
z23.object({
|
|
2995
|
+
outcome: z23.literal("go-ahead"),
|
|
2996
|
+
id: z23.string().uuid(),
|
|
2997
|
+
callId: z23.number().int().positive(),
|
|
2998
|
+
say: z23.string().min(1)
|
|
2999
|
+
}).strict(),
|
|
3000
|
+
z23.object({ outcome: z23.literal("refused"), reason: z23.enum(OWNER_WAIT_DECLARE_REFUSALS) }).strict()
|
|
3001
|
+
]);
|
|
3002
|
+
var ownerWaitAddedItemSchema = z23.object({
|
|
3003
|
+
id: z23.string().uuid(),
|
|
3004
|
+
words: z23.string().min(1),
|
|
3005
|
+
reason: z23.string().min(1),
|
|
3006
|
+
addedAt: utcIso8601
|
|
3007
|
+
}).strict();
|
|
3008
|
+
var ownerWaitListSchema = z23.object({
|
|
3009
|
+
fixed: z23.array(z23.object({ id: ownerWaitItemIdSchema, words: z23.string().min(1) }).strict()).length(OWNER_WAIT_FIXED_COUNT),
|
|
3010
|
+
added: z23.array(ownerWaitAddedItemSchema).max(OWNER_WAIT_ADDITIONS_MAX)
|
|
3011
|
+
}).strict();
|
|
3012
|
+
var ownerWaitDeclarationSchema = z23.object({
|
|
3013
|
+
id: z23.string().uuid(),
|
|
3014
|
+
fixedItem: ownerWaitItemIdSchema.nullable(),
|
|
3015
|
+
addition: z23.object({ id: z23.string().uuid(), words: z23.string().min(1) }).strict().nullable(),
|
|
3016
|
+
question: z23.string().min(1),
|
|
3017
|
+
recommendation: z23.string().min(1),
|
|
3018
|
+
task: buildTaskIdSchema.nullable(),
|
|
3019
|
+
declaredAt: utcIso8601
|
|
3020
|
+
}).strict().refine((d) => d.fixedItem === null !== (d.addition === null), {
|
|
3021
|
+
message: "names exactly one item: a fixed one or an added one",
|
|
3022
|
+
path: ["fixedItem"]
|
|
3023
|
+
});
|
|
3024
|
+
var ownerWaitDeclarationsSchema = z23.object({ declarations: z23.array(ownerWaitDeclarationSchema) }).strict();
|
|
3025
|
+
var OWNER_WAIT_TOO_MANY = `This project already has ${String(OWNER_WAIT_ADDITIONS_MAX)} things added to what waits for the owner. Ask the owner to take one off in the console first.`;
|
|
3026
|
+
|
|
3027
|
+
// ../events/dist/autonomy.js
|
|
3028
|
+
import { z as z24 } from "zod";
|
|
3029
|
+
var AUTONOMY_CHANGED_IN = ["console", "chat"];
|
|
3030
|
+
var autonomyChangedInSchema = z24.enum(AUTONOMY_CHANGED_IN);
|
|
3031
|
+
var autonomySettingsSchema = z24.object({
|
|
3032
|
+
on: z24.boolean(),
|
|
3033
|
+
// whether builds run on their own
|
|
3034
|
+
mayPublish: z24.boolean(),
|
|
3035
|
+
// whether builds may publish or release without waiting
|
|
3036
|
+
maySpend: z24.boolean(),
|
|
3037
|
+
// whether builds may spend money or sign up without waiting
|
|
3038
|
+
changedAt: utcIso8601.nullable(),
|
|
3039
|
+
// when the setting last changed; null if never
|
|
3040
|
+
changedIn: autonomyChangedInSchema.nullable()
|
|
3041
|
+
// where it last changed; null if never
|
|
3042
|
+
}).strict().refine((v) => v.changedAt === null === (v.changedIn === null), {
|
|
3043
|
+
message: "says when and where it changed, or neither",
|
|
3044
|
+
path: ["changedIn"]
|
|
3045
|
+
});
|
|
3046
|
+
var autonomyChangeRequestSchema = z24.object({
|
|
3047
|
+
on: z24.boolean(),
|
|
3048
|
+
// the switch
|
|
3049
|
+
mayPublish: z24.boolean(),
|
|
3050
|
+
// the publish box
|
|
3051
|
+
maySpend: z24.boolean()
|
|
3052
|
+
// the spend box
|
|
3053
|
+
}).strict();
|
|
3054
|
+
var AUTONOMY_CHAT_ACTS = ["on", "off"];
|
|
3055
|
+
var autonomyChatActSchema = z24.enum(AUTONOMY_CHAT_ACTS);
|
|
3056
|
+
var autonomyChatRequestSchema = z24.object({
|
|
3057
|
+
act: autonomyChatActSchema
|
|
3058
|
+
// what the owner typed
|
|
3059
|
+
}).strict();
|
|
3060
|
+
var autonomyChatResultSchema = z24.discriminatedUnion("outcome", [
|
|
3061
|
+
z24.object({
|
|
3062
|
+
outcome: z24.literal("recorded"),
|
|
3063
|
+
on: z24.boolean()
|
|
3064
|
+
// whether builds now run on their own
|
|
3065
|
+
}).strict(),
|
|
3066
|
+
z24.object({ outcome: z24.literal("not-owner") }).strict(),
|
|
3067
|
+
z24.object({ outcome: z24.literal("no-owner") }).strict(),
|
|
3068
|
+
z24.object({ outcome: z24.literal("not-offered") }).strict(),
|
|
3069
|
+
z24.object({ outcome: z24.literal("unavailable") }).strict()
|
|
3070
|
+
]);
|
|
3071
|
+
var autonomyStateSchema = z24.object({
|
|
3072
|
+
on: z24.boolean(),
|
|
3073
|
+
// whether builds run on their own
|
|
3074
|
+
mayPublish: z24.boolean(),
|
|
3075
|
+
// whether builds may publish or release without waiting
|
|
3076
|
+
maySpend: z24.boolean()
|
|
3077
|
+
// whether builds may spend money or sign up without waiting
|
|
3078
|
+
}).strict();
|
|
3079
|
+
var autonomyStateFileSchema = autonomyStateSchema.extend({
|
|
3080
|
+
readAt: utcIso8601
|
|
3081
|
+
// when this copy was read from Halfcycle
|
|
3082
|
+
}).strict();
|
|
3083
|
+
var AUTONOMY_STATE_FILE_MAX_AGE_MS = 24 * 60 * 60 * 1e3;
|
|
3084
|
+
var AUTONOMY_ACTIVITY_KINDS = ["went-ahead", "waiting", "approved", "closed", "waiting-on-owner"];
|
|
3085
|
+
var autonomyActivityKindSchema = z24.enum(AUTONOMY_ACTIVITY_KINDS);
|
|
3086
|
+
var autonomyActivityRowSchema = z24.object({
|
|
3087
|
+
kind: autonomyActivityKindSchema,
|
|
3088
|
+
// what happened
|
|
3089
|
+
phase: scopePhaseSchema,
|
|
3090
|
+
// the phase it happened in
|
|
3091
|
+
question: ownerWaitText.optional(),
|
|
3092
|
+
// the agent's question, for a decision
|
|
3093
|
+
chose: ownerWaitText.optional(),
|
|
3094
|
+
// what the agent chose or recommends, for a decision
|
|
3095
|
+
undoable: ownerWaitUndoableSchema.optional(),
|
|
3096
|
+
// whether the agent says it can be undone
|
|
3097
|
+
how: statementText(STATEMENT_MAX).optional(),
|
|
3098
|
+
// how to undo it, when it can be
|
|
3099
|
+
at: utcIso8601
|
|
3100
|
+
// when it happened
|
|
3101
|
+
}).strict().superRefine((row, ctx) => {
|
|
3102
|
+
const decision = row.kind === "went-ahead" || row.kind === "waiting";
|
|
3103
|
+
if (decision && row.question === void 0) {
|
|
3104
|
+
ctx.addIssue({ code: "custom", message: "a decision says its question", path: ["question"] });
|
|
3105
|
+
}
|
|
3106
|
+
if (!decision) {
|
|
3107
|
+
for (const key of ["question", "chose", "undoable", "how"]) {
|
|
3108
|
+
if (row[key] !== void 0) {
|
|
3109
|
+
ctx.addIssue({ code: "custom", message: "only a decision carries this", path: [key] });
|
|
3110
|
+
}
|
|
3111
|
+
}
|
|
3112
|
+
}
|
|
3113
|
+
if (row.kind === "went-ahead") {
|
|
3114
|
+
if (row.chose === void 0) {
|
|
3115
|
+
ctx.addIssue({ code: "custom", message: "a decision that went ahead says what was chosen", path: ["chose"] });
|
|
3116
|
+
}
|
|
3117
|
+
if (row.undoable === void 0) {
|
|
3118
|
+
ctx.addIssue({
|
|
3119
|
+
code: "custom",
|
|
3120
|
+
message: "a decision that went ahead says whether it can be undone",
|
|
3121
|
+
path: ["undoable"]
|
|
3122
|
+
});
|
|
3123
|
+
}
|
|
3124
|
+
}
|
|
3125
|
+
if (row.kind === "waiting" && (row.undoable !== void 0 || row.how !== void 0)) {
|
|
3126
|
+
ctx.addIssue({ code: "custom", message: "a waiting decision has not been made yet", path: ["undoable"] });
|
|
3127
|
+
}
|
|
3128
|
+
if (row.kind === "went-ahead" && row.undoable === "yes" !== (row.how !== void 0)) {
|
|
3129
|
+
ctx.addIssue({ code: "custom", message: "says how to undo it exactly when it can be undone", path: ["how"] });
|
|
3130
|
+
}
|
|
3131
|
+
});
|
|
3132
|
+
var autonomyActivitySchema = z24.object({
|
|
3133
|
+
rows: z24.array(autonomyActivityRowSchema)
|
|
3134
|
+
// newest first
|
|
3135
|
+
}).strict();
|
|
3136
|
+
var savedIdSchema = z24.string().min(1).max(64).regex(/^[a-z0-9]+(?:-[a-z0-9]+)*$/, "must be lower-case letters, digits and single hyphens");
|
|
3137
|
+
var SAVED_REF_PREFIX = "refs/halfcycle/saved/";
|
|
3138
|
+
function savedRefOf(id) {
|
|
3139
|
+
return `${SAVED_REF_PREFIX}${id}`;
|
|
3140
|
+
}
|
|
3141
|
+
var SAVED_KEEP_MS = 7 * 24 * 60 * 60 * 1e3;
|
|
3142
|
+
function isProjectRelativePath(p) {
|
|
3143
|
+
if (p.length === 0 || hasControlCharacter(p))
|
|
3144
|
+
return false;
|
|
3145
|
+
if (p.startsWith("/") || p.startsWith("\\") || /^[A-Za-z]:/.test(p))
|
|
3146
|
+
return false;
|
|
3147
|
+
return p.split(/[\\/]/).every((segment) => segment !== "" && windowsNameOf(segment) !== "" && !isGitFolderSegment(segment));
|
|
3148
|
+
}
|
|
3149
|
+
function windowsNameOf(segment) {
|
|
3150
|
+
return segment.replace(/[\u200b-\u200f\u202a-\u202e\u2060-\u2064\u206a-\u206f\ufeff]/g, "").split(":")[0].replace(/[. ]+$/, "").toLowerCase();
|
|
3151
|
+
}
|
|
3152
|
+
function isGitFolderSegment(segment) {
|
|
3153
|
+
const t = windowsNameOf(segment);
|
|
3154
|
+
return t === ".git" || /^git~\d+$/.test(t);
|
|
3155
|
+
}
|
|
3156
|
+
var SAVED_PATH_KEPT_IN = ["commit", "copy"];
|
|
3157
|
+
var savedPathSchema = z24.object({
|
|
3158
|
+
path: z24.string().max(4096).refine(isProjectRelativePath, { message: "must be a path inside the project" }),
|
|
3159
|
+
// the file, relative to the project
|
|
3160
|
+
keptIn: z24.enum(SAVED_PATH_KEPT_IN)
|
|
3161
|
+
// in the save's commit, or copied beside this file
|
|
3162
|
+
}).strict();
|
|
3163
|
+
var savedCommitSchema = z24.string().regex(/^(?:[0-9a-f]{40}|[0-9a-f]{64})$/, "must be a full commit hash");
|
|
3164
|
+
var savedBranchSchema = z24.object({
|
|
3165
|
+
name: z24.string().min(1).max(255).refine((n) => !hasControlCharacter(n) && !n.startsWith("-") && !n.includes("..") && n !== "HEAD", {
|
|
3166
|
+
message: "must be a branch name"
|
|
3167
|
+
}),
|
|
3168
|
+
// the branch's name
|
|
3169
|
+
commit: savedCommitSchema
|
|
3170
|
+
// the commit it pointed at
|
|
3171
|
+
}).strict();
|
|
3172
|
+
var savedSchema = z24.object({
|
|
3173
|
+
id: savedIdSchema,
|
|
3174
|
+
// what restore is typed with
|
|
3175
|
+
savedAt: utcIso8601,
|
|
3176
|
+
// when the copy was made
|
|
3177
|
+
ref: z24.string().min(1),
|
|
3178
|
+
// where the saved commit is kept
|
|
3179
|
+
what: z24.string().min(1).max(2e3),
|
|
3180
|
+
// what the action would have changed, in words
|
|
3181
|
+
paths: z24.array(savedPathSchema),
|
|
3182
|
+
// every saved file
|
|
3183
|
+
branches: z24.array(savedBranchSchema)
|
|
3184
|
+
// every saved line of work
|
|
3185
|
+
}).strict().refine((s) => s.ref === savedRefOf(s.id), { message: "must be the ref of this save", path: ["ref"] });
|
|
3186
|
+
function autonomyC8(agent, question, chose) {
|
|
3187
|
+
return `${agent} decided "${question}" without asking you, under your autonomy setting: chose "${chose}". It is on the list of things that wait for you, so it was sent to you as it was made.`;
|
|
3188
|
+
}
|
|
3189
|
+
|
|
2901
3190
|
// ../events/dist/work-words.js
|
|
2902
3191
|
var CALLS_HEADING = "Calls made without asking you";
|
|
2903
3192
|
var CALLS_EXPLAINED = "These are the decisions the agents recorded taking on their own, each against a stated default. Halfcycle lists what they recorded; it cannot see a call an agent did not record.";
|
|
@@ -3207,44 +3496,68 @@ function mintIdentity(targetRepoRoot) {
|
|
|
3207
3496
|
};
|
|
3208
3497
|
}
|
|
3209
3498
|
function repairRemoteCredential(targetRepoRoot) {
|
|
3210
|
-
const
|
|
3499
|
+
const repaired = plannedRemoteRepair(targetRepoRoot);
|
|
3500
|
+
if (repaired !== null)
|
|
3501
|
+
replaceProjectIdentityFile(targetRepoRoot, repaired);
|
|
3502
|
+
}
|
|
3503
|
+
function plannedRemoteRepair(targetRepoRoot) {
|
|
3504
|
+
const path = join3(targetRepoRoot, PROJECT_IDENTITY_REL);
|
|
3211
3505
|
let onDisk;
|
|
3212
3506
|
try {
|
|
3213
3507
|
const parsed = JSON.parse(readFileSync2(path, "utf-8"));
|
|
3214
3508
|
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed))
|
|
3215
|
-
return;
|
|
3509
|
+
return null;
|
|
3216
3510
|
onDisk = parsed;
|
|
3217
3511
|
} catch {
|
|
3218
|
-
return;
|
|
3512
|
+
return null;
|
|
3219
3513
|
}
|
|
3220
3514
|
const storedRemote = onDisk.remote ?? null;
|
|
3221
3515
|
const remote = redactRemoteCredential(storedRemote);
|
|
3222
3516
|
if (remote === storedRemote)
|
|
3223
|
-
return;
|
|
3224
|
-
|
|
3517
|
+
return null;
|
|
3518
|
+
return serializeProjectIdentity({
|
|
3225
3519
|
projectId: onDisk.projectId ?? "",
|
|
3226
3520
|
rootCommit: onDisk.rootCommit ?? null,
|
|
3227
3521
|
remote,
|
|
3228
3522
|
note: PROJECT_IDENTITY_NOTE
|
|
3229
|
-
};
|
|
3523
|
+
});
|
|
3524
|
+
}
|
|
3525
|
+
function replaceProjectIdentityFile(targetRepoRoot, text) {
|
|
3526
|
+
const path = join3(targetRepoRoot, PROJECT_IDENTITY_REL);
|
|
3230
3527
|
const temporary = `${path}.${process.pid}.${randomUUID()}.tmp`;
|
|
3231
3528
|
try {
|
|
3232
|
-
writeFileSync2(temporary,
|
|
3529
|
+
writeFileSync2(temporary, text, { encoding: "utf-8", flag: "wx" });
|
|
3233
3530
|
renameSync(temporary, path);
|
|
3234
3531
|
} catch {
|
|
3235
3532
|
} finally {
|
|
3236
3533
|
rmSync(temporary, { force: true });
|
|
3237
3534
|
}
|
|
3238
3535
|
}
|
|
3536
|
+
function planProjectIdentity(targetRepoRoot) {
|
|
3537
|
+
const path = join3(targetRepoRoot, PROJECT_IDENTITY_REL);
|
|
3538
|
+
if (!existsSync(path)) {
|
|
3539
|
+
return { action: "add", after: serializeProjectIdentity(mintIdentity(targetRepoRoot)) };
|
|
3540
|
+
}
|
|
3541
|
+
const repaired = plannedRemoteRepair(targetRepoRoot);
|
|
3542
|
+
if (repaired !== null)
|
|
3543
|
+
return { action: "change", after: repaired };
|
|
3544
|
+
let current;
|
|
3545
|
+
try {
|
|
3546
|
+
current = readFileSync2(path, "utf-8");
|
|
3547
|
+
} catch {
|
|
3548
|
+
current = null;
|
|
3549
|
+
}
|
|
3550
|
+
return { action: "leave", after: current };
|
|
3551
|
+
}
|
|
3239
3552
|
function serializeProjectIdentity(identity) {
|
|
3240
3553
|
return JSON.stringify(identity, null, 2) + "\n";
|
|
3241
3554
|
}
|
|
3242
|
-
function createProjectIdentityFile(targetRepoRoot) {
|
|
3555
|
+
function createProjectIdentityFile(targetRepoRoot, text) {
|
|
3243
3556
|
const path = join3(targetRepoRoot, PROJECT_IDENTITY_REL);
|
|
3244
3557
|
mkdirSync2(dirname2(path), { recursive: true });
|
|
3245
3558
|
const temporary = `${path}.${process.pid}.${randomUUID()}.tmp`;
|
|
3246
3559
|
try {
|
|
3247
|
-
writeFileSync2(temporary, serializeProjectIdentity(mintIdentity(targetRepoRoot)), {
|
|
3560
|
+
writeFileSync2(temporary, text ?? serializeProjectIdentity(mintIdentity(targetRepoRoot)), {
|
|
3248
3561
|
encoding: "utf-8",
|
|
3249
3562
|
flag: "wx"
|
|
3250
3563
|
});
|
|
@@ -3259,6 +3572,10 @@ function createProjectIdentityFile(targetRepoRoot) {
|
|
|
3259
3572
|
}
|
|
3260
3573
|
}
|
|
3261
3574
|
|
|
3575
|
+
// dist/install-plan.js
|
|
3576
|
+
import { existsSync as existsSync3, readFileSync as readFileSync4 } from "node:fs";
|
|
3577
|
+
import { join as join5, posix } from "node:path";
|
|
3578
|
+
|
|
3262
3579
|
// dist/install-manifest.js
|
|
3263
3580
|
import { Buffer as Buffer2 } from "node:buffer";
|
|
3264
3581
|
import { createHash } from "node:crypto";
|
|
@@ -3557,6 +3874,469 @@ function union(a, b) {
|
|
|
3557
3874
|
return [.../* @__PURE__ */ new Set([...a, ...b])];
|
|
3558
3875
|
}
|
|
3559
3876
|
|
|
3877
|
+
// dist/scan.js
|
|
3878
|
+
import { existsSync as existsSync2, mkdirSync as mkdirSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync3 } from "node:fs";
|
|
3879
|
+
import { join as join4 } from "node:path";
|
|
3880
|
+
var HALFCYCLE_STATE_FORMAT = "halfcycle-state-file/v1";
|
|
3881
|
+
var HALFCYCLE_STATE_NOTE = [
|
|
3882
|
+
"This file is a local cache written ONCE when Halfcycle was installed. It is not a",
|
|
3883
|
+
"record of what this project has done, and it is never updated as work happens.",
|
|
3884
|
+
'An empty or missing "layers" means the install-time scan found no pre-existing',
|
|
3885
|
+
"artefacts on disk \u2014 it does NOT mean nothing has been done since, and it is not",
|
|
3886
|
+
"evidence that any step is unfinished or any approval is outstanding.",
|
|
3887
|
+
"The authoritative position lives in the Halfcycle service: call the",
|
|
3888
|
+
"halfcycle_resolve tool to ask where this project stands. Do not answer that",
|
|
3889
|
+
"question from this file."
|
|
3890
|
+
].join(" ");
|
|
3891
|
+
var LAYER_ARTEFACT_PATHS = {
|
|
3892
|
+
l0: ["ENGAGEMENT.md"],
|
|
3893
|
+
l1: ["docs/product-spec.md"],
|
|
3894
|
+
l2: ["docs/architecture-overview.md"],
|
|
3895
|
+
l3: ["docs/invariants.md"],
|
|
3896
|
+
l4: ["docs/features"],
|
|
3897
|
+
l5: ["docs/phases"]
|
|
3898
|
+
};
|
|
3899
|
+
function scanLayers(targetRepo, scannedAt = (/* @__PURE__ */ new Date()).toISOString()) {
|
|
3900
|
+
const layers = {};
|
|
3901
|
+
for (const [layer, paths] of Object.entries(LAYER_ARTEFACT_PATHS)) {
|
|
3902
|
+
const found = paths.filter((p) => existsSync2(join4(targetRepo, p)));
|
|
3903
|
+
if (found.length > 0) {
|
|
3904
|
+
layers[layer] = {
|
|
3905
|
+
value: "asserted",
|
|
3906
|
+
source: "bootstrap-scan",
|
|
3907
|
+
foundPaths: found,
|
|
3908
|
+
scannedAt
|
|
3909
|
+
};
|
|
3910
|
+
}
|
|
3911
|
+
}
|
|
3912
|
+
return { format: HALFCYCLE_STATE_FORMAT, note: HALFCYCLE_STATE_NOTE, layers };
|
|
3913
|
+
}
|
|
3914
|
+
var BOOTSTRAP_STATE_REL = ".halfcycle/state.json";
|
|
3915
|
+
function runBootstrapScan(targetRepo) {
|
|
3916
|
+
const planned = planBootstrapScan(targetRepo);
|
|
3917
|
+
if (planned.text !== null)
|
|
3918
|
+
writeBootstrapState(targetRepo, planned.text);
|
|
3919
|
+
return { state: planned.state, ran: planned.text !== null };
|
|
3920
|
+
}
|
|
3921
|
+
function planBootstrapScan(targetRepo) {
|
|
3922
|
+
const statePath = join4(targetRepo, BOOTSTRAP_STATE_REL);
|
|
3923
|
+
if (existsSync2(statePath)) {
|
|
3924
|
+
const existing = JSON.parse(readFileSync3(statePath, "utf-8"));
|
|
3925
|
+
return { state: existing, text: null };
|
|
3926
|
+
}
|
|
3927
|
+
const state = scanLayers(targetRepo);
|
|
3928
|
+
return { state, text: JSON.stringify(state, null, 2) + "\n" };
|
|
3929
|
+
}
|
|
3930
|
+
function writeBootstrapState(targetRepo, text) {
|
|
3931
|
+
mkdirSync3(join4(targetRepo, ".halfcycle"), { recursive: true });
|
|
3932
|
+
writeFileSync3(join4(targetRepo, BOOTSTRAP_STATE_REL), text, "utf-8");
|
|
3933
|
+
}
|
|
3934
|
+
function mergeWithRecords(state, layersWithRealRecords) {
|
|
3935
|
+
const merged = {};
|
|
3936
|
+
const allLayers = /* @__PURE__ */ new Set([
|
|
3937
|
+
...Object.keys(state.layers),
|
|
3938
|
+
...layersWithRealRecords
|
|
3939
|
+
]);
|
|
3940
|
+
for (const layer of allLayers) {
|
|
3941
|
+
if (layersWithRealRecords.has(layer)) {
|
|
3942
|
+
merged[layer] = "complete";
|
|
3943
|
+
} else if (state.layers[layer]) {
|
|
3944
|
+
merged[layer] = "asserted";
|
|
3945
|
+
}
|
|
3946
|
+
}
|
|
3947
|
+
return merged;
|
|
3948
|
+
}
|
|
3949
|
+
|
|
3950
|
+
// dist/install-plan.js
|
|
3951
|
+
var PENDING = /* @__PURE__ */ Symbol("pending");
|
|
3952
|
+
var YOURS = "a file of yours with the same name; Halfcycle will not touch it";
|
|
3953
|
+
var SAME = "already as Halfcycle would write it";
|
|
3954
|
+
var PREVIEW_WORDS = Object.freeze({
|
|
3955
|
+
command: {
|
|
3956
|
+
add: "a command your coding agent can run to take the next Halfcycle step",
|
|
3957
|
+
change: "a newer version of this Halfcycle command",
|
|
3958
|
+
leave: SAME,
|
|
3959
|
+
yours: YOURS
|
|
3960
|
+
},
|
|
3961
|
+
"check-script": {
|
|
3962
|
+
add: "a check Halfcycle runs before and after your agent's actions",
|
|
3963
|
+
change: "a newer version of this Halfcycle check",
|
|
3964
|
+
leave: SAME,
|
|
3965
|
+
yours: YOURS
|
|
3966
|
+
},
|
|
3967
|
+
settings: {
|
|
3968
|
+
add: "your coding agent's settings, with the checks Halfcycle runs",
|
|
3969
|
+
change: "adds the lines Halfcycle needs; your own lines stay as they are",
|
|
3970
|
+
leave: "already has the lines Halfcycle needs",
|
|
3971
|
+
yours: YOURS
|
|
3972
|
+
},
|
|
3973
|
+
"server-entry": {
|
|
3974
|
+
add: "tells your coding agent how to reach Halfcycle's service",
|
|
3975
|
+
change: "adds Halfcycle's entry; your own entries stay as they are",
|
|
3976
|
+
leave: "already has Halfcycle's entry",
|
|
3977
|
+
yours: YOURS
|
|
3978
|
+
},
|
|
3979
|
+
"ignore-list": {
|
|
3980
|
+
add: "keeps Halfcycle's files for this computer only out of git",
|
|
3981
|
+
change: "adds the lines Halfcycle needs; your own lines stay as they are",
|
|
3982
|
+
leave: "already has the lines Halfcycle needs",
|
|
3983
|
+
yours: YOURS
|
|
3984
|
+
},
|
|
3985
|
+
"line-endings": {
|
|
3986
|
+
add: "keeps Halfcycle's checks working in a Windows copy of this project",
|
|
3987
|
+
change: "adds the lines Halfcycle needs; your own lines stay as they are",
|
|
3988
|
+
leave: "already has the lines Halfcycle needs",
|
|
3989
|
+
yours: YOURS
|
|
3990
|
+
},
|
|
3991
|
+
"halfcycle-folder": {
|
|
3992
|
+
add: "a file in Halfcycle's own folder",
|
|
3993
|
+
change: "a newer version of this file in Halfcycle's own folder",
|
|
3994
|
+
leave: SAME,
|
|
3995
|
+
yours: YOURS
|
|
3996
|
+
},
|
|
3997
|
+
runner: {
|
|
3998
|
+
add: "the program Halfcycle's checks run",
|
|
3999
|
+
change: "a newer version of the program Halfcycle's checks run",
|
|
4000
|
+
leave: SAME,
|
|
4001
|
+
yours: YOURS
|
|
4002
|
+
},
|
|
4003
|
+
"captured-index": {
|
|
4004
|
+
add: "the list of real examples your project's tests are checked against",
|
|
4005
|
+
change: "a newer version of this list",
|
|
4006
|
+
leave: SAME,
|
|
4007
|
+
yours: YOURS
|
|
4008
|
+
},
|
|
4009
|
+
"old-sign-in": {
|
|
4010
|
+
add: "an older Halfcycle sign-in",
|
|
4011
|
+
change: "takes out an older Halfcycle sign-in; your own lines stay as they are",
|
|
4012
|
+
leave: "an older Halfcycle sign-in that cannot be moved yet; left as it is",
|
|
4013
|
+
yours: YOURS
|
|
4014
|
+
}
|
|
4015
|
+
});
|
|
4016
|
+
var kindTable;
|
|
4017
|
+
function kinds() {
|
|
4018
|
+
kindTable ??= /* @__PURE__ */ new Map([
|
|
4019
|
+
[".claude/hooks/guard-runner.sh", "check-script"],
|
|
4020
|
+
[".claude/hooks/user-prompt-reminder.sh", "check-script"],
|
|
4021
|
+
[".claude/hooks/user-prompt-scope.sh", "check-script"],
|
|
4022
|
+
[".claude/hooks/user-prompt-findings.sh", "check-script"],
|
|
4023
|
+
[".claude/hooks/session-end-marker.sh", "check-script"],
|
|
4024
|
+
[".claude/hooks/session-start-marker.sh", "check-script"],
|
|
4025
|
+
[SETTINGS_REL, "settings"],
|
|
4026
|
+
[MCP_REGISTRATION_REL, "server-entry"],
|
|
4027
|
+
[GITIGNORE_REL, "ignore-list"],
|
|
4028
|
+
[GITATTRIBUTES_REL, "line-endings"],
|
|
4029
|
+
[VENDORED_BIN_REL, "runner"],
|
|
4030
|
+
[CREW_ROSTER_REL, "halfcycle-folder"],
|
|
4031
|
+
[BUNDLE_PIN_REL, "halfcycle-folder"],
|
|
4032
|
+
[PROJECT_IDENTITY_REL, "halfcycle-folder"],
|
|
4033
|
+
[BOOTSTRAP_STATE_REL, "halfcycle-folder"],
|
|
4034
|
+
[INSTALL_MANIFEST_REL, "halfcycle-folder"],
|
|
4035
|
+
[RETIRED_MCP_HEADERS_HELPER_REL, "halfcycle-folder"],
|
|
4036
|
+
[LEGACY_CI_STANZA_REL, "halfcycle-folder"],
|
|
4037
|
+
[CAPTURED_INDEX_REL, "captured-index"],
|
|
4038
|
+
[LEGACY_ENV_LOCAL_REL, "old-sign-in"]
|
|
4039
|
+
]);
|
|
4040
|
+
return kindTable;
|
|
4041
|
+
}
|
|
4042
|
+
function previewKindOf(rel) {
|
|
4043
|
+
if (/^\.claude\/commands\/[^/]+\.md$/.test(rel))
|
|
4044
|
+
return "command";
|
|
4045
|
+
return kinds().get(rel);
|
|
4046
|
+
}
|
|
4047
|
+
function kindOf(rel) {
|
|
4048
|
+
const kind = previewKindOf(rel);
|
|
4049
|
+
if (kind === void 0)
|
|
4050
|
+
throw new Error(`[halfcycle] the installer plans ${rel}, which has no preview words.`);
|
|
4051
|
+
return kind;
|
|
4052
|
+
}
|
|
4053
|
+
var STEPS = /* @__PURE__ */ new WeakMap();
|
|
4054
|
+
function planInstall(options) {
|
|
4055
|
+
const { targetRepo, engagementId, engagementType, credential, home, env, createdByAccountId } = options;
|
|
4056
|
+
const preview = engagementId === void 0;
|
|
4057
|
+
if (!preview) {
|
|
4058
|
+
assertEngagementId(engagementId);
|
|
4059
|
+
if (engagementType === void 0) {
|
|
4060
|
+
throw new Error("[halfcycle] a plan for a project needs its kind as well as its id.");
|
|
4061
|
+
}
|
|
4062
|
+
}
|
|
4063
|
+
if (!existsSync3(targetRepo)) {
|
|
4064
|
+
throw new Error(`[halfcycle] Target repo does not exist: ${targetRepo}`);
|
|
4065
|
+
}
|
|
4066
|
+
const writeSet = closedWriteSet();
|
|
4067
|
+
const previous = readPreviousManifest(targetRepo);
|
|
4068
|
+
const dirsBefore = new Set([...writeSet.dirs].filter((dir) => existsSync3(join5(targetRepo, dir))));
|
|
4069
|
+
const { version, commands } = planCommandStubs(targetRepo, previous, writeSet);
|
|
4070
|
+
const vendored = planVendoredBinary(targetRepo);
|
|
4071
|
+
let settings;
|
|
4072
|
+
try {
|
|
4073
|
+
settings = planSettings(targetRepo, previous, home, env);
|
|
4074
|
+
} catch (err) {
|
|
4075
|
+
if (err instanceof SettingsParseRefused)
|
|
4076
|
+
return Object.freeze({ entries: [], outside: [], refusal: err.message });
|
|
4077
|
+
throw err;
|
|
4078
|
+
}
|
|
4079
|
+
const hooks = planHookScripts(targetRepo);
|
|
4080
|
+
const ciStanza = planLegacyCiStanzaRemoval(targetRepo);
|
|
4081
|
+
const mcp = planMcpRegistration(targetRepo, preview ? "pending" : credential);
|
|
4082
|
+
const mcpHelper = mcp.kind === "none" ? null : planMcpHeadersHelperRemoval(targetRepo);
|
|
4083
|
+
const gitignore = planGitignore(targetRepo);
|
|
4084
|
+
const gitattributes = planGitattributes(targetRepo);
|
|
4085
|
+
const identity = planProjectIdentity(targetRepo);
|
|
4086
|
+
if (identity.after === null)
|
|
4087
|
+
readFileSync4(join5(targetRepo, PROJECT_IDENTITY_REL));
|
|
4088
|
+
const credentialPlan = !preview && credential !== void 0 ? planEngagementCredential(credential, engagementId, home) : null;
|
|
4089
|
+
const credentialWrite = credentialPlan === null ? null : { outcome: credentialPlan.outcome, existed: credentialPlan.existed, path: engagementEnvPath(engagementId, home) };
|
|
4090
|
+
const storedAfter = credentialPlan?.storedAfter ?? (preview ? Object.fromEntries(ENGAGEMENT_ENV_KEYS.map((key) => [key, "supplied at sign-in"])) : readEngagementEnv(engagementId, home) ?? {});
|
|
4091
|
+
const legacy = migrateLegacyEnvLocal(targetRepo, storedAfter);
|
|
4092
|
+
const pin = preview ? planBundlePinPreview(targetRepo, version, home) : planBundlePin(targetRepo, version, engagementId, engagementType, createdByAccountId, credential?.methodVersion);
|
|
4093
|
+
const crew = planCrewRoster(targetRepo);
|
|
4094
|
+
const scan = { text: planBootstrapScan(targetRepo).text };
|
|
4095
|
+
const steps = {
|
|
4096
|
+
targetRepo,
|
|
4097
|
+
version,
|
|
4098
|
+
engagementId,
|
|
4099
|
+
engagementType,
|
|
4100
|
+
credential: preview ? void 0 : credential,
|
|
4101
|
+
home,
|
|
4102
|
+
writeSet,
|
|
4103
|
+
previous,
|
|
4104
|
+
dirsBefore,
|
|
4105
|
+
commands,
|
|
4106
|
+
vendored,
|
|
4107
|
+
settings,
|
|
4108
|
+
hooks,
|
|
4109
|
+
ciStanza,
|
|
4110
|
+
mcp,
|
|
4111
|
+
mcpHelper,
|
|
4112
|
+
gitignore,
|
|
4113
|
+
gitattributes,
|
|
4114
|
+
identity,
|
|
4115
|
+
credentialWrite,
|
|
4116
|
+
legacy,
|
|
4117
|
+
pin,
|
|
4118
|
+
crew,
|
|
4119
|
+
scan
|
|
4120
|
+
};
|
|
4121
|
+
const record2 = preview ? null : planInstallRecord(targetRepo, accountInstall(steps, predicted(steps)).manifestText);
|
|
4122
|
+
const plan = Object.freeze({
|
|
4123
|
+
entries: Object.freeze(entriesOf(steps, record2)),
|
|
4124
|
+
outside: Object.freeze(outsideOf(steps))
|
|
4125
|
+
});
|
|
4126
|
+
STEPS.set(plan, steps);
|
|
4127
|
+
return plan;
|
|
4128
|
+
}
|
|
4129
|
+
function predicted(steps) {
|
|
4130
|
+
const after = /* @__PURE__ */ new Map();
|
|
4131
|
+
const dirs = /* @__PURE__ */ new Set();
|
|
4132
|
+
const lands = (rel, text, writes) => {
|
|
4133
|
+
after.set(rel, text);
|
|
4134
|
+
if (!writes)
|
|
4135
|
+
return;
|
|
4136
|
+
for (let dir = posix.dirname(rel); dir !== "."; dir = posix.dirname(dir))
|
|
4137
|
+
dirs.add(dir);
|
|
4138
|
+
};
|
|
4139
|
+
const file = (planned) => {
|
|
4140
|
+
if (planned.outcome !== "collided")
|
|
4141
|
+
lands(planned.rel, planned.text, planned.outcome === "written");
|
|
4142
|
+
};
|
|
4143
|
+
steps.commands.forEach(file);
|
|
4144
|
+
file(steps.vendored);
|
|
4145
|
+
file(steps.settings.file);
|
|
4146
|
+
steps.hooks.forEach(file);
|
|
4147
|
+
if (steps.mcp.kind === "write")
|
|
4148
|
+
lands(MCP_REGISTRATION_REL, steps.mcp.text, steps.mcp.outcome !== "skipped");
|
|
4149
|
+
if (steps.identity.after !== null)
|
|
4150
|
+
lands(PROJECT_IDENTITY_REL, steps.identity.after, steps.identity.action === "add");
|
|
4151
|
+
if (steps.pin !== null)
|
|
4152
|
+
file(steps.pin);
|
|
4153
|
+
file(steps.crew);
|
|
4154
|
+
if (steps.scan.text !== null)
|
|
4155
|
+
lands(BOOTSTRAP_STATE_REL, steps.scan.text, true);
|
|
4156
|
+
const mergeAfter = (planned) => planned.outcome === "written" ? planned.after : planned.before;
|
|
4157
|
+
return {
|
|
4158
|
+
ciStanza: steps.ciStanza,
|
|
4159
|
+
mcpHelper: steps.mcpHelper,
|
|
4160
|
+
gitignore: steps.gitignore.outcome,
|
|
4161
|
+
gitignoreAfter: mergeAfter(steps.gitignore),
|
|
4162
|
+
gitattributes: steps.gitattributes.outcome,
|
|
4163
|
+
gitattributesAfter: mergeAfter(steps.gitattributes),
|
|
4164
|
+
credential: steps.credentialWrite?.outcome ?? null,
|
|
4165
|
+
legacy: steps.legacy,
|
|
4166
|
+
bytesAfter: (rel) => {
|
|
4167
|
+
const text = after.get(rel);
|
|
4168
|
+
if (text === void 0)
|
|
4169
|
+
throw new Error(`[halfcycle] the plan has no bytes for ${rel}.`);
|
|
4170
|
+
return Buffer.from(text, "utf-8");
|
|
4171
|
+
},
|
|
4172
|
+
dirExistsAfter: (dir) => steps.dirsBefore.has(dir) || dirs.has(dir),
|
|
4173
|
+
stateExistsAfter: true
|
|
4174
|
+
};
|
|
4175
|
+
}
|
|
4176
|
+
function entriesOf(steps, record2) {
|
|
4177
|
+
const { targetRepo } = steps;
|
|
4178
|
+
const entries = [];
|
|
4179
|
+
const fromFile = (planned) => {
|
|
4180
|
+
const kind = kindOf(planned.rel);
|
|
4181
|
+
if (planned.outcome === "written") {
|
|
4182
|
+
entries.push({ path: planned.rel, action: planned.replacedExisting ? "change" : "add", kind, bytes: planned.text });
|
|
4183
|
+
} else if (planned.outcome === "collided") {
|
|
4184
|
+
entries.push({ path: planned.rel, action: "leave", kind, bytes: null, yours: true });
|
|
4185
|
+
} else {
|
|
4186
|
+
entries.push({ path: planned.rel, action: "leave", kind, bytes: null });
|
|
4187
|
+
}
|
|
4188
|
+
};
|
|
4189
|
+
const retired = (rel, decided) => {
|
|
4190
|
+
if (decided === null || decided === "absent")
|
|
4191
|
+
return;
|
|
4192
|
+
if (decided === "removed")
|
|
4193
|
+
entries.push({ path: rel, action: "remove", kind: kindOf(rel), bytes: null });
|
|
4194
|
+
else if (decided === "kept-foreign")
|
|
4195
|
+
entries.push({ path: rel, action: "leave", kind: kindOf(rel), bytes: null, yours: true });
|
|
4196
|
+
else
|
|
4197
|
+
entries.push({ path: rel, action: "leave", kind: kindOf(rel), bytes: null });
|
|
4198
|
+
};
|
|
4199
|
+
const merge = (rel, planned) => {
|
|
4200
|
+
if (planned.outcome === "written" && planned.after !== null) {
|
|
4201
|
+
entries.push({ path: rel, action: planned.before === null ? "add" : "change", kind: kindOf(rel), bytes: planned.after });
|
|
4202
|
+
} else if (planned.before !== null || planned.outcome === "failed") {
|
|
4203
|
+
entries.push({ path: rel, action: "leave", kind: kindOf(rel), bytes: null });
|
|
4204
|
+
}
|
|
4205
|
+
};
|
|
4206
|
+
const pending = (rel) => {
|
|
4207
|
+
const there = existsSync3(join5(targetRepo, rel));
|
|
4208
|
+
entries.push({ path: rel, action: there ? "change" : "add", kind: kindOf(rel), bytes: PENDING });
|
|
4209
|
+
};
|
|
4210
|
+
steps.commands.forEach(fromFile);
|
|
4211
|
+
fromFile(steps.vendored);
|
|
4212
|
+
const settings = steps.settings;
|
|
4213
|
+
entries.push(settings.file.outcome === "written" ? { path: SETTINGS_REL, action: settings.preexisted ? "change" : "add", kind: "settings", bytes: settings.file.text } : { path: SETTINGS_REL, action: "leave", kind: "settings", bytes: null });
|
|
4214
|
+
steps.hooks.forEach(fromFile);
|
|
4215
|
+
retired(LEGACY_CI_STANZA_REL, steps.ciStanza);
|
|
4216
|
+
const mcp = steps.mcp;
|
|
4217
|
+
if (mcp.kind === "write") {
|
|
4218
|
+
const action = mcp.outcome === "written" ? "add" : mcp.outcome === "merged" ? "change" : "leave";
|
|
4219
|
+
entries.push({ path: MCP_REGISTRATION_REL, action, kind: "server-entry", bytes: action === "leave" ? null : mcp.text });
|
|
4220
|
+
} else if (mcp.kind === "pending") {
|
|
4221
|
+
pending(MCP_REGISTRATION_REL);
|
|
4222
|
+
} else {
|
|
4223
|
+
entries.push({ path: MCP_REGISTRATION_REL, action: "leave", kind: "server-entry", bytes: null });
|
|
4224
|
+
}
|
|
4225
|
+
retired(RETIRED_MCP_HEADERS_HELPER_REL, steps.mcpHelper);
|
|
4226
|
+
merge(GITIGNORE_REL, steps.gitignore);
|
|
4227
|
+
merge(GITATTRIBUTES_REL, steps.gitattributes);
|
|
4228
|
+
const identity = steps.identity;
|
|
4229
|
+
entries.push({
|
|
4230
|
+
path: PROJECT_IDENTITY_REL,
|
|
4231
|
+
action: identity.action,
|
|
4232
|
+
kind: "halfcycle-folder",
|
|
4233
|
+
bytes: identity.action === "leave" ? null : identity.after
|
|
4234
|
+
});
|
|
4235
|
+
const legacy = steps.legacy;
|
|
4236
|
+
if (legacy.outcome === "removed") {
|
|
4237
|
+
entries.push({ path: LEGACY_ENV_LOCAL_REL, action: "remove", kind: "old-sign-in", bytes: null });
|
|
4238
|
+
} else if (legacy.outcome === "stripped") {
|
|
4239
|
+
entries.push({ path: LEGACY_ENV_LOCAL_REL, action: "change", kind: "old-sign-in", bytes: legacy.keptText ?? null });
|
|
4240
|
+
} else if (legacy.outcome !== "absent") {
|
|
4241
|
+
entries.push({ path: LEGACY_ENV_LOCAL_REL, action: "leave", kind: "old-sign-in", bytes: null });
|
|
4242
|
+
}
|
|
4243
|
+
if (steps.pin === null)
|
|
4244
|
+
pending(BUNDLE_PIN_REL);
|
|
4245
|
+
else
|
|
4246
|
+
fromFile(steps.pin);
|
|
4247
|
+
fromFile(steps.crew);
|
|
4248
|
+
entries.push(steps.scan.text !== null ? { path: BOOTSTRAP_STATE_REL, action: "add", kind: "halfcycle-folder", bytes: steps.scan.text } : { path: BOOTSTRAP_STATE_REL, action: "leave", kind: "halfcycle-folder", bytes: null });
|
|
4249
|
+
if (record2 === null)
|
|
4250
|
+
pending(INSTALL_MANIFEST_REL);
|
|
4251
|
+
else
|
|
4252
|
+
fromFile(record2);
|
|
4253
|
+
return entries;
|
|
4254
|
+
}
|
|
4255
|
+
function outsideOf(steps) {
|
|
4256
|
+
if (steps.engagementId === void 0) {
|
|
4257
|
+
const outside = [{ kind: "sign-in", path: null, action: "pending" }];
|
|
4258
|
+
if (!projectPinned(steps.targetRepo))
|
|
4259
|
+
outside.push({ kind: "new-project" });
|
|
4260
|
+
return outside;
|
|
4261
|
+
}
|
|
4262
|
+
const write = steps.credentialWrite;
|
|
4263
|
+
if (write === null)
|
|
4264
|
+
return [];
|
|
4265
|
+
const action = write.outcome === "skipped" ? "leave" : write.existed ? "change" : "add";
|
|
4266
|
+
return [{ kind: "sign-in", path: write.path, action }];
|
|
4267
|
+
}
|
|
4268
|
+
function projectPinned(targetRepo) {
|
|
4269
|
+
try {
|
|
4270
|
+
const id = readBundlePin(targetRepo)?.engagementId;
|
|
4271
|
+
return typeof id === "string" && id.trim() !== "";
|
|
4272
|
+
} catch {
|
|
4273
|
+
return false;
|
|
4274
|
+
}
|
|
4275
|
+
}
|
|
4276
|
+
function applyInstallPlan(plan) {
|
|
4277
|
+
if (plan.refusal !== void 0)
|
|
4278
|
+
throw new SettingsParseRefused(plan.refusal);
|
|
4279
|
+
const steps = STEPS.get(plan);
|
|
4280
|
+
if (steps === void 0)
|
|
4281
|
+
throw new Error("[halfcycle] only a plan made by planInstall can be applied.");
|
|
4282
|
+
const { targetRepo, engagementId, pin } = steps;
|
|
4283
|
+
if (engagementId === void 0 || pin === null || steps.mcp.kind === "pending") {
|
|
4284
|
+
throw new Error("[halfcycle] a preview cannot be applied: it is still waiting for the service.");
|
|
4285
|
+
}
|
|
4286
|
+
for (const command of steps.commands)
|
|
4287
|
+
putPlannedFile(targetRepo, command);
|
|
4288
|
+
putPlannedFile(targetRepo, steps.vendored);
|
|
4289
|
+
putPlannedFile(targetRepo, steps.settings.file);
|
|
4290
|
+
for (const hook of steps.hooks)
|
|
4291
|
+
putPlannedFile(targetRepo, hook);
|
|
4292
|
+
const ciStanza = steps.ciStanza === "removed" ? removePlannedRetiredFile(targetRepo, LEGACY_CI_STANZA_REL) : steps.ciStanza;
|
|
4293
|
+
putPlannedMcp(targetRepo, steps.mcp);
|
|
4294
|
+
const mcpHelper = steps.mcpHelper === "removed" ? removePlannedRetiredFile(targetRepo, RETIRED_MCP_HEADERS_HELPER_REL) : steps.mcpHelper;
|
|
4295
|
+
const gitignore = putPlannedGitignore(targetRepo, steps.gitignore);
|
|
4296
|
+
const gitignorePath = join5(targetRepo, GITIGNORE_REL);
|
|
4297
|
+
const gitignoreAfter = existsSync3(gitignorePath) ? readFileSync4(gitignorePath, "utf-8") : null;
|
|
4298
|
+
const gitattributes = putPlannedGitattributes(targetRepo, steps.gitattributes);
|
|
4299
|
+
const gitattributesAfter = readTextOrNull(join5(targetRepo, GITATTRIBUTES_REL));
|
|
4300
|
+
writeProjectIdentity(targetRepo, steps.identity);
|
|
4301
|
+
const credential = steps.credential;
|
|
4302
|
+
const credentialOutcome = steps.credentialWrite !== null && credential !== void 0 ? writeEngagementCredential(credential, engagementId, steps.home) : null;
|
|
4303
|
+
const stored = readEngagementEnv(engagementId, steps.home) ?? {};
|
|
4304
|
+
const legacy = applyLegacyMigration(targetRepo, migrateLegacyEnvLocal(targetRepo, stored));
|
|
4305
|
+
writeBundlePin(targetRepo, pin, []);
|
|
4306
|
+
putPlannedFile(targetRepo, steps.crew);
|
|
4307
|
+
if (steps.scan.text !== null)
|
|
4308
|
+
writeBootstrapState(targetRepo, steps.scan.text);
|
|
4309
|
+
const { report, installManifest, manifestText } = accountInstall(steps, {
|
|
4310
|
+
ciStanza,
|
|
4311
|
+
mcpHelper,
|
|
4312
|
+
gitignore,
|
|
4313
|
+
gitignoreAfter,
|
|
4314
|
+
gitattributes,
|
|
4315
|
+
gitattributesAfter,
|
|
4316
|
+
credential: credentialOutcome,
|
|
4317
|
+
legacy,
|
|
4318
|
+
bytesAfter: (rel) => readFileSync4(join5(targetRepo, rel)),
|
|
4319
|
+
dirExistsAfter: (dir) => existsSync3(join5(targetRepo, dir)),
|
|
4320
|
+
stateExistsAfter: existsSync3(join5(targetRepo, BOOTSTRAP_STATE_REL))
|
|
4321
|
+
});
|
|
4322
|
+
const record2 = planInstallRecord(targetRepo, manifestText);
|
|
4323
|
+
putPlannedFile(targetRepo, record2);
|
|
4324
|
+
recordInstallRecord(report, record2);
|
|
4325
|
+
const notShared = findNotShared(targetRepo, installManifest, report.collidedPaths);
|
|
4326
|
+
return {
|
|
4327
|
+
version: steps.version,
|
|
4328
|
+
writtenPaths: report.writtenPaths,
|
|
4329
|
+
skippedPaths: report.skippedPaths,
|
|
4330
|
+
mergedPaths: report.mergedPaths,
|
|
4331
|
+
collidedPaths: report.collidedPaths,
|
|
4332
|
+
editedCollidedPaths: report.editedCollidedPaths,
|
|
4333
|
+
replacedPaths: report.replacedPaths,
|
|
4334
|
+
bootstrapScanRan: steps.scan.text !== null,
|
|
4335
|
+
legacyCredentialKept: legacy.outcome === "kept" ? { keys: legacy.unbacked, missing: legacy.missingForAdoption } : null,
|
|
4336
|
+
notShared
|
|
4337
|
+
};
|
|
4338
|
+
}
|
|
4339
|
+
|
|
3560
4340
|
// dist/mcp-endpoint.js
|
|
3561
4341
|
var MCP_ENDPOINT_PATH = "/mcp";
|
|
3562
4342
|
function mcpEndpointUrl(origin) {
|
|
@@ -3636,71 +4416,6 @@ function mergePermissions(existing, generated, retiredOurs) {
|
|
|
3636
4416
|
return result;
|
|
3637
4417
|
}
|
|
3638
4418
|
|
|
3639
|
-
// dist/scan.js
|
|
3640
|
-
import { existsSync as existsSync2, mkdirSync as mkdirSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync3 } from "node:fs";
|
|
3641
|
-
import { join as join4 } from "node:path";
|
|
3642
|
-
var HALFCYCLE_STATE_FORMAT = "halfcycle-state-file/v1";
|
|
3643
|
-
var HALFCYCLE_STATE_NOTE = [
|
|
3644
|
-
"This file is a local cache written ONCE when Halfcycle was installed. It is not a",
|
|
3645
|
-
"record of what this project has done, and it is never updated as work happens.",
|
|
3646
|
-
'An empty or missing "layers" means the install-time scan found no pre-existing',
|
|
3647
|
-
"artefacts on disk \u2014 it does NOT mean nothing has been done since, and it is not",
|
|
3648
|
-
"evidence that any step is unfinished or any approval is outstanding.",
|
|
3649
|
-
"The authoritative position lives in the Halfcycle service: call the",
|
|
3650
|
-
"halfcycle_resolve tool to ask where this project stands. Do not answer that",
|
|
3651
|
-
"question from this file."
|
|
3652
|
-
].join(" ");
|
|
3653
|
-
var LAYER_ARTEFACT_PATHS = {
|
|
3654
|
-
l0: ["ENGAGEMENT.md"],
|
|
3655
|
-
l1: ["docs/product-spec.md"],
|
|
3656
|
-
l2: ["docs/architecture-overview.md"],
|
|
3657
|
-
l3: ["docs/invariants.md"],
|
|
3658
|
-
l4: ["docs/features"],
|
|
3659
|
-
l5: ["docs/phases"]
|
|
3660
|
-
};
|
|
3661
|
-
function scanLayers(targetRepo, scannedAt = (/* @__PURE__ */ new Date()).toISOString()) {
|
|
3662
|
-
const layers = {};
|
|
3663
|
-
for (const [layer, paths] of Object.entries(LAYER_ARTEFACT_PATHS)) {
|
|
3664
|
-
const found = paths.filter((p) => existsSync2(join4(targetRepo, p)));
|
|
3665
|
-
if (found.length > 0) {
|
|
3666
|
-
layers[layer] = {
|
|
3667
|
-
value: "asserted",
|
|
3668
|
-
source: "bootstrap-scan",
|
|
3669
|
-
foundPaths: found,
|
|
3670
|
-
scannedAt
|
|
3671
|
-
};
|
|
3672
|
-
}
|
|
3673
|
-
}
|
|
3674
|
-
return { format: HALFCYCLE_STATE_FORMAT, note: HALFCYCLE_STATE_NOTE, layers };
|
|
3675
|
-
}
|
|
3676
|
-
var BOOTSTRAP_STATE_REL = ".halfcycle/state.json";
|
|
3677
|
-
function runBootstrapScan(targetRepo) {
|
|
3678
|
-
const statePath = join4(targetRepo, BOOTSTRAP_STATE_REL);
|
|
3679
|
-
if (existsSync2(statePath)) {
|
|
3680
|
-
const existing = JSON.parse(readFileSync3(statePath, "utf-8"));
|
|
3681
|
-
return { state: existing, ran: false };
|
|
3682
|
-
}
|
|
3683
|
-
const state = scanLayers(targetRepo);
|
|
3684
|
-
mkdirSync3(join4(targetRepo, ".halfcycle"), { recursive: true });
|
|
3685
|
-
writeFileSync3(statePath, JSON.stringify(state, null, 2) + "\n", "utf-8");
|
|
3686
|
-
return { state, ran: true };
|
|
3687
|
-
}
|
|
3688
|
-
function mergeWithRecords(state, layersWithRealRecords) {
|
|
3689
|
-
const merged = {};
|
|
3690
|
-
const allLayers = /* @__PURE__ */ new Set([
|
|
3691
|
-
...Object.keys(state.layers),
|
|
3692
|
-
...layersWithRealRecords
|
|
3693
|
-
]);
|
|
3694
|
-
for (const layer of allLayers) {
|
|
3695
|
-
if (layersWithRealRecords.has(layer)) {
|
|
3696
|
-
merged[layer] = "complete";
|
|
3697
|
-
} else if (state.layers[layer]) {
|
|
3698
|
-
merged[layer] = "asserted";
|
|
3699
|
-
}
|
|
3700
|
-
}
|
|
3701
|
-
return merged;
|
|
3702
|
-
}
|
|
3703
|
-
|
|
3704
4419
|
// dist/install.js
|
|
3705
4420
|
var ZONE_B_DIR = ".workbench";
|
|
3706
4421
|
var __filename = fileURLToPath(import.meta.url);
|
|
@@ -3708,36 +4423,36 @@ var __dirname = dirname3(__filename);
|
|
|
3708
4423
|
function findBundleRoot(startDir) {
|
|
3709
4424
|
let candidate = startDir;
|
|
3710
4425
|
for (let i = 0; i < 4; i++) {
|
|
3711
|
-
const manifest =
|
|
4426
|
+
const manifest = join6(candidate, ".claude-plugin", "plugin.json");
|
|
3712
4427
|
try {
|
|
3713
|
-
if (
|
|
4428
|
+
if (existsSync4(manifest))
|
|
3714
4429
|
return candidate;
|
|
3715
4430
|
} catch {
|
|
3716
4431
|
}
|
|
3717
4432
|
candidate = dirname3(candidate);
|
|
3718
4433
|
}
|
|
3719
|
-
return
|
|
4434
|
+
return join6(startDir, "..");
|
|
3720
4435
|
}
|
|
3721
4436
|
var BUNDLE_ROOT = findBundleRoot(__dirname);
|
|
3722
4437
|
function resolveVendoredBinary() {
|
|
3723
4438
|
const candidates = [
|
|
3724
|
-
|
|
3725
|
-
|
|
4439
|
+
join6(BUNDLE_ROOT, "bin", "bin.bundle.mjs"),
|
|
4440
|
+
join6(BUNDLE_ROOT, "..", "runner", "dist", "bin.bundle.mjs")
|
|
3726
4441
|
];
|
|
3727
4442
|
for (const candidate of candidates) {
|
|
3728
|
-
if (
|
|
4443
|
+
if (existsSync4(candidate))
|
|
3729
4444
|
return candidate;
|
|
3730
4445
|
}
|
|
3731
4446
|
throw new Error(`[halfcycle] Cannot find the self-contained guard binary bin.bundle.mjs. This copy of the halfcycle package is incomplete \u2014 reinstall it, or rebuild it if you are working on it. Looked in: ${candidates.join(", ")}.`);
|
|
3732
4447
|
}
|
|
3733
4448
|
function resolvePreFilterShell() {
|
|
3734
4449
|
const candidates = [
|
|
3735
|
-
|
|
3736
|
-
|
|
4450
|
+
join6(BUNDLE_ROOT, "bin", PRE_FILTER_SHELL_FILENAME),
|
|
4451
|
+
join6(BUNDLE_ROOT, "..", "runner", "dist", PRE_FILTER_SHELL_FILENAME)
|
|
3737
4452
|
];
|
|
3738
4453
|
for (const candidate of candidates) {
|
|
3739
|
-
if (
|
|
3740
|
-
return
|
|
4454
|
+
if (existsSync4(candidate))
|
|
4455
|
+
return readFileSync5(candidate, "utf-8");
|
|
3741
4456
|
}
|
|
3742
4457
|
throw new Error(`[halfcycle] Cannot find ${PRE_FILTER_SHELL_FILENAME}. This copy of the halfcycle package is incomplete \u2014 reinstall it, or rebuild it if you are working on it. Looked in: ${candidates.join(", ")}.`);
|
|
3743
4458
|
}
|
|
@@ -3766,7 +4481,7 @@ function isAllowlisted(targetRelPath) {
|
|
|
3766
4481
|
var CREW_ROSTER_REL = ".halfcycle/crew.json";
|
|
3767
4482
|
var CAPTURED_INDEX_REL = "test/fixtures/captured/manifest.json";
|
|
3768
4483
|
function capturedIndexScaffold() {
|
|
3769
|
-
return
|
|
4484
|
+
return join6(BUNDLE_ROOT, "scaffolding", "test", "fixtures", "captured", "manifest.json");
|
|
3770
4485
|
}
|
|
3771
4486
|
var GITIGNORE_REL = ".gitignore";
|
|
3772
4487
|
var GITATTRIBUTES_REL = ".gitattributes";
|
|
@@ -3804,7 +4519,7 @@ function closedWriteSet() {
|
|
|
3804
4519
|
const merged = /* @__PURE__ */ new Set([SETTINGS_REL, MCP_REGISTRATION_REL, GITIGNORE_REL, GITATTRIBUTES_REL]);
|
|
3805
4520
|
const dirs = /* @__PURE__ */ new Set();
|
|
3806
4521
|
for (const path of [...files, ...merged]) {
|
|
3807
|
-
for (let dir =
|
|
4522
|
+
for (let dir = posix2.dirname(path); dir !== "."; dir = posix2.dirname(dir))
|
|
3808
4523
|
dirs.add(dir);
|
|
3809
4524
|
}
|
|
3810
4525
|
const ownedDirs = new Set([...dirs].filter((dir) => `${dir}/`.startsWith(INSTALLER_OWNED_DIR)));
|
|
@@ -3828,11 +4543,11 @@ function closedWriteSet() {
|
|
|
3828
4543
|
function sha256OfWhatThisVersionWrites(commands) {
|
|
3829
4544
|
const out = /* @__PURE__ */ new Map();
|
|
3830
4545
|
const fromFile = (rel, src) => {
|
|
3831
|
-
if (
|
|
3832
|
-
out.set(rel, fileSha256(
|
|
4546
|
+
if (existsSync4(src))
|
|
4547
|
+
out.set(rel, fileSha256(readFileSync5(src)));
|
|
3833
4548
|
};
|
|
3834
4549
|
for (const cmd of commands)
|
|
3835
|
-
fromFile(commandStubRel(cmd.name),
|
|
4550
|
+
fromFile(commandStubRel(cmd.name), join6(BUNDLE_ROOT, ".claude-plugin", cmd.path));
|
|
3836
4551
|
for (const [rel, content] of hookScriptsThisVersionWrites())
|
|
3837
4552
|
out.set(rel, fileSha256(content));
|
|
3838
4553
|
fromFile(CAPTURED_INDEX_REL, capturedIndexScaffold());
|
|
@@ -3845,17 +4560,17 @@ function bytesAreOurs(path, bytes, recorded, writeSet) {
|
|
|
3845
4560
|
}
|
|
3846
4561
|
function readPreviousManifest(targetRepo) {
|
|
3847
4562
|
try {
|
|
3848
|
-
return parseManifest(
|
|
4563
|
+
return parseManifest(readFileSync5(join6(targetRepo, INSTALL_MANIFEST_REL), "utf-8"));
|
|
3849
4564
|
} catch {
|
|
3850
4565
|
return null;
|
|
3851
4566
|
}
|
|
3852
4567
|
}
|
|
3853
|
-
function noteFile(draft,
|
|
4568
|
+
function noteFile(draft, rel, outcome, after) {
|
|
3854
4569
|
if (outcome === "collided") {
|
|
3855
4570
|
recordCollision(draft, rel);
|
|
3856
4571
|
return;
|
|
3857
4572
|
}
|
|
3858
|
-
recordInstalledFile(draft, rel,
|
|
4573
|
+
recordInstalledFile(draft, rel, after(rel));
|
|
3859
4574
|
}
|
|
3860
4575
|
function recordSettingsMerge(draft, existing, generated, preexisted, merged) {
|
|
3861
4576
|
const generatedHooks = generated.hooks ?? {};
|
|
@@ -3944,11 +4659,11 @@ function findNotShared(targetRepo, manifest, collidedPaths) {
|
|
|
3944
4659
|
const collided = new Set(collidedPaths);
|
|
3945
4660
|
const dirVerdicts = /* @__PURE__ */ new Map();
|
|
3946
4661
|
const travelsThroughPlainDirs = (rel) => {
|
|
3947
|
-
for (let dir =
|
|
4662
|
+
for (let dir = posix2.dirname(rel); dir !== "."; dir = posix2.dirname(dir)) {
|
|
3948
4663
|
let plain = dirVerdicts.get(dir);
|
|
3949
4664
|
if (plain === void 0) {
|
|
3950
|
-
const abs =
|
|
3951
|
-
plain = !lstatOrNull(abs)?.isSymbolicLink() && lstatOrNull(
|
|
4665
|
+
const abs = join6(targetRepo, dir);
|
|
4666
|
+
plain = !lstatOrNull(abs)?.isSymbolicLink() && lstatOrNull(join6(abs, ".git")) === null;
|
|
3952
4667
|
dirVerdicts.set(dir, plain);
|
|
3953
4668
|
}
|
|
3954
4669
|
if (!plain)
|
|
@@ -3962,7 +4677,7 @@ function findNotShared(targetRepo, manifest, collidedPaths) {
|
|
|
3962
4677
|
...merged,
|
|
3963
4678
|
INSTALL_MANIFEST_REL
|
|
3964
4679
|
])
|
|
3965
|
-
].filter((rel) => !collided.has(rel)).filter((rel) => lstatOrNull(
|
|
4680
|
+
].filter((rel) => !collided.has(rel)).filter((rel) => lstatOrNull(join6(targetRepo, rel)) !== null).filter(travelsThroughPlainDirs).sort();
|
|
3966
4681
|
const ignored = readIgnoredPaths(targetRepo, paths);
|
|
3967
4682
|
if (ignored === null)
|
|
3968
4683
|
return null;
|
|
@@ -3985,7 +4700,7 @@ function lstatOrNull(path) {
|
|
|
3985
4700
|
}
|
|
3986
4701
|
function readFileOrNull(path) {
|
|
3987
4702
|
try {
|
|
3988
|
-
return
|
|
4703
|
+
return readFileSync5(path, "utf-8");
|
|
3989
4704
|
} catch {
|
|
3990
4705
|
return null;
|
|
3991
4706
|
}
|
|
@@ -3994,28 +4709,33 @@ function isPlainObject(value) {
|
|
|
3994
4709
|
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
3995
4710
|
}
|
|
3996
4711
|
function readPluginManifest() {
|
|
3997
|
-
const manifestPath =
|
|
3998
|
-
const raw =
|
|
4712
|
+
const manifestPath = join6(BUNDLE_ROOT, ".claude-plugin", "plugin.json");
|
|
4713
|
+
const raw = readFileSync5(manifestPath, "utf-8");
|
|
3999
4714
|
return JSON.parse(raw);
|
|
4000
4715
|
}
|
|
4001
|
-
function
|
|
4002
|
-
|
|
4003
|
-
|
|
4004
|
-
|
|
4716
|
+
function planManifestCommands(manifest, targetRepo, previous, writeSet) {
|
|
4717
|
+
const recordedFiles = new Map((previous?.files ?? []).filter((file) => writeSet.files.has(file.path)).map((file) => [file.path, file]));
|
|
4718
|
+
return (manifest.commands ?? []).map((cmd) => {
|
|
4719
|
+
const srcFile = join6(BUNDLE_ROOT, ".claude-plugin", cmd.path);
|
|
4720
|
+
if (!existsSync4(srcFile)) {
|
|
4005
4721
|
throw new Error(`[halfcycle] plugin.json declares command "${cmd.name}" at "${cmd.path}", but no file exists there (${srcFile}). The manifest and commands/ must agree.`);
|
|
4006
4722
|
}
|
|
4007
4723
|
const rel = commandStubRel(cmd.name);
|
|
4008
|
-
const
|
|
4009
|
-
const
|
|
4010
|
-
const
|
|
4011
|
-
|
|
4012
|
-
|
|
4013
|
-
|
|
4014
|
-
|
|
4015
|
-
|
|
4016
|
-
|
|
4017
|
-
|
|
4724
|
+
const content = readFileSync5(srcFile, "utf-8");
|
|
4725
|
+
const recorded = recordedFiles.get(rel);
|
|
4726
|
+
const planned = decideCollisionSafe(join6(targetRepo, rel), targetRepo, content, (current) => bytesAreOurs(rel, current, recorded, writeSet));
|
|
4727
|
+
return { ...planned, editedCollided: planned.outcome === "collided" && recorded !== void 0 };
|
|
4728
|
+
});
|
|
4729
|
+
}
|
|
4730
|
+
function putPlannedFile(targetRepoRoot, planned) {
|
|
4731
|
+
if (planned.outcome !== "written")
|
|
4732
|
+
return;
|
|
4733
|
+
if (!isAllowlisted(planned.rel)) {
|
|
4734
|
+
throw new Error(`[halfcycle] Write-allowlist violation: attempted to write "${planned.rel}". Only the method surface and scaffolding paths are writable.`);
|
|
4018
4735
|
}
|
|
4736
|
+
const target = join6(targetRepoRoot, planned.rel);
|
|
4737
|
+
mkdirSync4(dirname3(target), { recursive: true });
|
|
4738
|
+
writeFileSync4(target, planned.text, "utf-8");
|
|
4019
4739
|
}
|
|
4020
4740
|
function writeAllowlisted(targetAbsPath, targetRepoRoot, content, writtenPaths) {
|
|
4021
4741
|
const rel = relative(targetRepoRoot, targetAbsPath).replace(/\\/g, "/");
|
|
@@ -4028,76 +4748,58 @@ function writeAllowlisted(targetAbsPath, targetRepoRoot, content, writtenPaths)
|
|
|
4028
4748
|
writeFileSync4(targetAbsPath, content, "utf-8");
|
|
4029
4749
|
writtenPaths.push(rel);
|
|
4030
4750
|
}
|
|
4031
|
-
function
|
|
4751
|
+
function decideCollisionSafe(targetAbsPath, targetRepoRoot, content, isOurs) {
|
|
4032
4752
|
const rel = relative(targetRepoRoot, targetAbsPath).replace(/\\/g, "/");
|
|
4033
4753
|
if (!isAllowlisted(rel)) {
|
|
4034
4754
|
throw new Error(`[halfcycle] Write-allowlist violation: attempted to write "${rel}". Only the method surface and scaffolding paths are writable.`);
|
|
4035
4755
|
}
|
|
4036
|
-
|
|
4037
|
-
const current = existsSync3(targetAbsPath) ? readFileSync4(targetAbsPath) : null;
|
|
4756
|
+
const current = existsSync4(targetAbsPath) ? readFileSync5(targetAbsPath) : null;
|
|
4038
4757
|
const text = inLineEndingOf(current?.toString("utf-8") ?? null, content);
|
|
4039
|
-
if (current
|
|
4040
|
-
|
|
4041
|
-
|
|
4042
|
-
|
|
4043
|
-
|
|
4044
|
-
replacedExisting
|
|
4045
|
-
}
|
|
4046
|
-
mkdirSync4(dirname3(targetAbsPath), { recursive: true });
|
|
4047
|
-
writeFileSync4(targetAbsPath, text, "utf-8");
|
|
4048
|
-
return { outcome: "written", replacedExisting };
|
|
4758
|
+
if (current === null)
|
|
4759
|
+
return { rel, text, outcome: "written", replacedExisting: false };
|
|
4760
|
+
if (current.toString("utf-8") === text)
|
|
4761
|
+
return { rel, text, outcome: "skipped", replacedExisting: false };
|
|
4762
|
+
if (!isOurs(current))
|
|
4763
|
+
return { rel, text, outcome: "collided", replacedExisting: false };
|
|
4764
|
+
return { rel, text, outcome: "written", replacedExisting: true };
|
|
4049
4765
|
}
|
|
4050
4766
|
var INSTALLER_OWNED_DIR = ".halfcycle/";
|
|
4051
|
-
function
|
|
4767
|
+
function decideOwned(targetAbsPath, targetRepoRoot, content) {
|
|
4052
4768
|
const rel = relative(targetRepoRoot, targetAbsPath).replace(/\\/g, "/");
|
|
4053
4769
|
if (!isAllowlisted(rel)) {
|
|
4054
4770
|
throw new Error(`[halfcycle] Write-allowlist violation: attempted to write "${rel}". Only the method surface and scaffolding paths are writable.`);
|
|
4055
4771
|
}
|
|
4056
4772
|
if (!rel.startsWith(INSTALLER_OWNED_DIR)) {
|
|
4057
|
-
throw new Error(`[halfcycle] Path-ownership violation: attempted to claim "${rel}" by path. Only ${INSTALLER_OWNED_DIR} is owned outright by this installer; everywhere else a differing file may be a developer's, so ownership must be proved from the file's own contents (
|
|
4773
|
+
throw new Error(`[halfcycle] Path-ownership violation: attempted to claim "${rel}" by path. Only ${INSTALLER_OWNED_DIR} is owned outright by this installer; everywhere else a differing file may be a developer's, so ownership must be proved from the file's own contents (decideOwnedGenerated) rather than assumed from where it sits.`);
|
|
4058
4774
|
}
|
|
4059
4775
|
const current = readFileOrNull(targetAbsPath);
|
|
4060
4776
|
const text = inLineEndingOf(current, content);
|
|
4061
4777
|
if (current === text)
|
|
4062
|
-
return "skipped";
|
|
4063
|
-
|
|
4064
|
-
writeFileSync4(targetAbsPath, text, "utf-8");
|
|
4065
|
-
return "written";
|
|
4778
|
+
return { rel, text, outcome: "skipped", replacedExisting: false };
|
|
4779
|
+
return { rel, text, outcome: "written", replacedExisting: existsSync4(targetAbsPath) };
|
|
4066
4780
|
}
|
|
4067
4781
|
var GENERATED_HEADER_LINE_INDEX = 1;
|
|
4068
|
-
function
|
|
4782
|
+
function decideOwnedGenerated(targetAbsPath, targetRepoRoot, content, generatedMarker) {
|
|
4069
4783
|
const rel = relative(targetRepoRoot, targetAbsPath).replace(/\\/g, "/");
|
|
4070
4784
|
if (!isAllowlisted(rel)) {
|
|
4071
4785
|
throw new Error(`[halfcycle] Write-allowlist violation: attempted to write "${rel}". Only the method surface and scaffolding paths are writable.`);
|
|
4072
4786
|
}
|
|
4073
|
-
|
|
4074
|
-
|
|
4075
|
-
|
|
4076
|
-
|
|
4077
|
-
|
|
4078
|
-
|
|
4079
|
-
|
|
4080
|
-
|
|
4081
|
-
replacedExisting = true;
|
|
4787
|
+
if (!existsSync4(targetAbsPath))
|
|
4788
|
+
return { rel, text: content, outcome: "written", replacedExisting: false };
|
|
4789
|
+
const current = readFileSync5(targetAbsPath, "utf-8");
|
|
4790
|
+
if (current === content)
|
|
4791
|
+
return { rel, text: content, outcome: "skipped", replacedExisting: false };
|
|
4792
|
+
const headerLine = current.split("\n")[GENERATED_HEADER_LINE_INDEX] ?? "";
|
|
4793
|
+
if (!headerLine.startsWith(generatedMarker)) {
|
|
4794
|
+
return { rel, text: content, outcome: "collided", replacedExisting: false };
|
|
4082
4795
|
}
|
|
4083
|
-
|
|
4084
|
-
writeFileSync4(targetAbsPath, content, "utf-8");
|
|
4085
|
-
return { outcome: "written", replacedExisting };
|
|
4796
|
+
return { rel, text: content, outcome: "written", replacedExisting: true };
|
|
4086
4797
|
}
|
|
4087
|
-
function recordOwnedGenerated(report,
|
|
4088
|
-
|
|
4089
|
-
|
|
4090
|
-
|
|
4091
|
-
|
|
4092
|
-
return outcome;
|
|
4093
|
-
}
|
|
4094
|
-
function recordOwned(report, targetAbsPath, targetRepoRoot, content, rel) {
|
|
4095
|
-
const existedBefore = existsSync3(targetAbsPath);
|
|
4096
|
-
const outcome = writeOwned(targetAbsPath, targetRepoRoot, content);
|
|
4097
|
-
record(report, outcome, rel);
|
|
4098
|
-
if (outcome === "written" && existedBefore)
|
|
4099
|
-
report.replacedPaths.push(rel);
|
|
4100
|
-
return outcome;
|
|
4798
|
+
function recordOwnedGenerated(report, planned) {
|
|
4799
|
+
record(report, planned.outcome, planned.rel);
|
|
4800
|
+
if (planned.replacedExisting)
|
|
4801
|
+
report.replacedPaths.push(planned.rel);
|
|
4802
|
+
return planned.outcome;
|
|
4101
4803
|
}
|
|
4102
4804
|
function record(report, outcome, rel) {
|
|
4103
4805
|
const bucket = {
|
|
@@ -4114,8 +4816,8 @@ var DENY_PATTERNS = [
|
|
|
4114
4816
|
];
|
|
4115
4817
|
var USER_PROMPT_REMINDER_SENTENCE = "Where this project stands, and what order to work in, is recorded in the Halfcycle service and nowhere in this repository \u2014 call halfcycle_resolve before you answer or plan, because the files here will look complete and current and still be wrong about position.";
|
|
4116
4818
|
function readExistingSettings(targetRepo) {
|
|
4117
|
-
const settingsPath =
|
|
4118
|
-
return
|
|
4819
|
+
const settingsPath = join6(targetRepo, SETTINGS_REL);
|
|
4820
|
+
return existsSync4(settingsPath) ? JSON.parse(readFileSync5(settingsPath, "utf-8")) : void 0;
|
|
4119
4821
|
}
|
|
4120
4822
|
var SettingsParseRefused = class extends Error {
|
|
4121
4823
|
constructor(message) {
|
|
@@ -4124,11 +4826,11 @@ var SettingsParseRefused = class extends Error {
|
|
|
4124
4826
|
}
|
|
4125
4827
|
};
|
|
4126
4828
|
function settingsParseRefusal(targetRepo, err) {
|
|
4127
|
-
const file =
|
|
4829
|
+
const file = join6(targetRepo, SETTINGS_REL);
|
|
4128
4830
|
const offset = /position (\d+)/.exec(err instanceof Error ? err.message : "")?.[1];
|
|
4129
4831
|
let where = "";
|
|
4130
4832
|
if (offset !== void 0) {
|
|
4131
|
-
const before =
|
|
4833
|
+
const before = readFileSync5(file, "utf-8").slice(0, Number(offset)).split("\n");
|
|
4132
4834
|
where = ` (line ${before.length}, column ${before[before.length - 1].length + 1})`;
|
|
4133
4835
|
}
|
|
4134
4836
|
return `${file} could not be read as JSON${where} \u2014 fix it and run "npx halfcycle" again.`;
|
|
@@ -4139,18 +4841,18 @@ var HALFCYCLE_STATUS_LINE = Object.freeze({
|
|
|
4139
4841
|
command: 'bash "$CLAUDE_PROJECT_DIR"/.claude/hooks/guard-runner.sh statusline'
|
|
4140
4842
|
});
|
|
4141
4843
|
function statusLineSetElsewhere(targetRepo, home = homedir2(), env = process.env) {
|
|
4142
|
-
const places = [
|
|
4844
|
+
const places = [join6(targetRepo, ".claude", "settings.local.json"), join6(home, ".claude", "settings.json")];
|
|
4143
4845
|
const configDir = env["CLAUDE_CONFIG_DIR"];
|
|
4144
4846
|
if (configDir !== void 0 && configDir !== "") {
|
|
4145
|
-
const expanded = configDir === "~" ? home : configDir.startsWith("~/") ?
|
|
4146
|
-
places.push(
|
|
4847
|
+
const expanded = configDir === "~" ? home : configDir.startsWith("~/") ? join6(home, configDir.slice(2)) : configDir;
|
|
4848
|
+
places.push(join6(isAbsolute2(expanded) ? expanded : resolve(expanded), "settings.json"));
|
|
4147
4849
|
}
|
|
4148
4850
|
return places.some((path) => {
|
|
4149
|
-
if (!
|
|
4851
|
+
if (!existsSync4(path))
|
|
4150
4852
|
return false;
|
|
4151
4853
|
let value;
|
|
4152
4854
|
try {
|
|
4153
|
-
value = JSON.parse(
|
|
4855
|
+
value = JSON.parse(readFileSync5(path, "utf-8"));
|
|
4154
4856
|
} catch {
|
|
4155
4857
|
return true;
|
|
4156
4858
|
}
|
|
@@ -4387,6 +5089,10 @@ ${GENERATED_SESSION_END_HEADER} the Halfcycle installer.
|
|
|
4387
5089
|
# written at session start. Deletion happens HERE and not at stop time: the stop
|
|
4388
5090
|
# hook runs at the end of every turn, and deleting the marker there would throw
|
|
4389
5091
|
# away the session's starting point for every later turn.
|
|
5092
|
+
#
|
|
5093
|
+
# Also removes the two small files this session may have left behind for its
|
|
5094
|
+
# checks to hand findings across turns, both named by this session's id alone,
|
|
5095
|
+
# with no fallback name to remove when there is none.
|
|
4390
5096
|
INPUT="$(cat 2>/dev/null || true)"
|
|
4391
5097
|
|
|
4392
5098
|
REPO_ROOT="\${CLAUDE_PROJECT_DIR:-$(git rev-parse --show-toplevel 2>/dev/null)}"
|
|
@@ -4407,6 +5113,8 @@ rm -f "\${MARKER_DIR}/halfcycle-session-\${HASH}.eval" 2>/dev/null || true
|
|
|
4407
5113
|
if [ -n "$SESSION_ID" ]; then
|
|
4408
5114
|
rm -f "\${MARKER_DIR}/halfcycle-session-\${HASH}-\${SESSION_ID}.ref" 2>/dev/null || true
|
|
4409
5115
|
rm -f "\${MARKER_DIR}/halfcycle-session-\${HASH}-\${SESSION_ID}.eval" 2>/dev/null || true
|
|
5116
|
+
rm -f "\${MARKER_DIR}/halfcycle-findings-\${SESSION_ID}.json" 2>/dev/null || true
|
|
5117
|
+
rm -f "\${MARKER_DIR}/halfcycle-own-\${SESSION_ID}.jsonl" 2>/dev/null || true
|
|
4410
5118
|
fi
|
|
4411
5119
|
exit 0
|
|
4412
5120
|
`;
|
|
@@ -4466,19 +5174,20 @@ function generateUserPromptScopeHook() {
|
|
|
4466
5174
|
return `#!/usr/bin/env bash
|
|
4467
5175
|
${GENERATED_USER_PROMPT_SCOPE_HEADER} the Halfcycle installer, now also for the close review.
|
|
4468
5176
|
#
|
|
4469
|
-
# Lets the owner of this project approve a phase's scope, or send it back,
|
|
4470
|
-
# decide one part of a phase's close review,
|
|
4471
|
-
#
|
|
4472
|
-
#
|
|
4473
|
-
# "
|
|
5177
|
+
# Lets the owner of this project approve a phase's scope, or send it back,
|
|
5178
|
+
# decide one part of a phase's close review, or turn builds running on their
|
|
5179
|
+
# own on or off, by typing one line as the whole prompt in their own Claude
|
|
5180
|
+
# Code session: "approve <phase>", "send back <phase>: <note>", "accept
|
|
5181
|
+
# <phase>/<part>", "question <phase>/<part>: <words>", "carry <phase>/<part>:
|
|
5182
|
+
# <words>", "drop <phase>/<part>: <words>", "autonomy on" or "autonomy off".
|
|
4474
5183
|
#
|
|
4475
5184
|
# AN ORDINARY PROMPT STARTS NOTHING. This script reads the hook input once and
|
|
4476
5185
|
# looks only at the start of the prompt's value. Unless the prompt begins, after
|
|
4477
5186
|
# any whitespace (typed, or escaped in the input), with "approve ", "send back ",
|
|
4478
|
-
# "accept ", "question ", "carry " or "
|
|
4479
|
-
# output, and Node never starts. A quote inside any other field arrives
|
|
4480
|
-
# so no other field (the folder the session is in, for one) can read as
|
|
4481
|
-
# start of the prompt.
|
|
5187
|
+
# "accept ", "question ", "carry ", "drop " or "autonomy ", in any case, it exits
|
|
5188
|
+
# 0 with no output, and Node never starts. A quote inside any other field arrives
|
|
5189
|
+
# escaped, so no other field (the folder the session is in, for one) can read as
|
|
5190
|
+
# the start of the prompt.
|
|
4482
5191
|
#
|
|
4483
5192
|
# Otherwise it finds the project root from its own location, never from the
|
|
4484
5193
|
# folder the session has moved to, and hands the same input to the guard
|
|
@@ -4499,7 +5208,7 @@ IFS= read -r -d '' HC_INPUT || true
|
|
|
4499
5208
|
# \\u escape, or as the UTF-8 bytes of a Unicode space. Compared byte for byte.
|
|
4500
5209
|
HC_LEAD='[[:space:]]|\\\\[nrtf]|\\\\u(000[9a-d]|0020|00a0|1680|200[0-9a]|202[89f]|205f|3000|feff)'
|
|
4501
5210
|
HC_LEAD="$HC_LEAD|"$'\\xc2\\xa0|\\xe1\\x9a\\x80|\\xe2\\x80[\\x80-\\x8a\\xa8\\xa9\\xaf]|\\xe2\\x81\\x9f|\\xe3\\x80\\x80|\\xef\\xbb\\xbf'
|
|
4502
|
-
if ! printf '%s' "$HC_INPUT" | LC_ALL=C grep -Eiq "\\"prompt\\"[[:space:]]*:[[:space:]]*\\"($HC_LEAD)*(approve|send back|accept|question|carry|drop) "; then
|
|
5211
|
+
if ! printf '%s' "$HC_INPUT" | LC_ALL=C grep -Eiq "\\"prompt\\"[[:space:]]*:[[:space:]]*\\"($HC_LEAD)*(approve|send back|accept|question|carry|drop|autonomy) "; then
|
|
4503
5212
|
exit 0
|
|
4504
5213
|
fi
|
|
4505
5214
|
|
|
@@ -4512,6 +5221,52 @@ set +e
|
|
|
4512
5221
|
exec bash "$HC_PROJECT_ROOT/.claude/hooks/guard-runner.sh" hook user-prompt-submit <<<"$HC_INPUT"
|
|
4513
5222
|
`;
|
|
4514
5223
|
}
|
|
5224
|
+
var GENERATED_USER_PROMPT_FINDINGS_HEADER = "# Findings hand-over \u2014 generated by";
|
|
5225
|
+
function generateUserPromptFindingsHook() {
|
|
5226
|
+
return `#!/usr/bin/env bash
|
|
5227
|
+
${GENERATED_USER_PROMPT_FINDINGS_HEADER} the Halfcycle installer.
|
|
5228
|
+
#
|
|
5229
|
+
# At the person's next message, before the agent starts, hands it the details of
|
|
5230
|
+
# whatever Halfcycle's checks found at an earlier turn end, once.
|
|
5231
|
+
#
|
|
5232
|
+
# AN ORDINARY PROMPT STARTS NOTHING. With no findings file for this session, this
|
|
5233
|
+
# script reads its session id with a bash regex (no node) and exits 0 with no
|
|
5234
|
+
# output once it finds no file \u2014 node and grep never start.
|
|
5235
|
+
trap 'exit 0' ERR
|
|
5236
|
+
set -e
|
|
5237
|
+
|
|
5238
|
+
HC_INPUT=""
|
|
5239
|
+
IFS= read -r -d '' HC_INPUT || true
|
|
5240
|
+
|
|
5241
|
+
# The session id, read with a bash regex held in a variable (a quoted operand to
|
|
5242
|
+
# =~ is read as a literal string, not a pattern). An id holding anything outside
|
|
5243
|
+
# [A-Za-z0-9-] cannot close the match and leaves this empty, which is read below
|
|
5244
|
+
# exactly like no session id at all.
|
|
5245
|
+
HC_SESSION_RE='"session_id"[[:space:]]*:[[:space:]]*"([A-Za-z0-9-]+)"'
|
|
5246
|
+
HC_SESSION_ID=""
|
|
5247
|
+
if [[ "$HC_INPUT" =~ $HC_SESSION_RE ]]; then
|
|
5248
|
+
HC_SESSION_ID="\${BASH_REMATCH[1]}"
|
|
5249
|
+
fi
|
|
5250
|
+
if [ -z "$HC_SESSION_ID" ]; then
|
|
5251
|
+
exit 0
|
|
5252
|
+
fi
|
|
5253
|
+
|
|
5254
|
+
HC_FINDINGS_DIR="\${TMPDIR:-/tmp}"
|
|
5255
|
+
HC_FINDINGS_DIR="\${HC_FINDINGS_DIR%/}" # strip trailing slash (macOS TMPDIR ends in /)
|
|
5256
|
+
HC_FINDINGS_FILE="\${HC_FINDINGS_DIR}/halfcycle-findings-\${HC_SESSION_ID}.json"
|
|
5257
|
+
if [ ! -f "$HC_FINDINGS_FILE" ]; then
|
|
5258
|
+
exit 0
|
|
5259
|
+
fi
|
|
5260
|
+
|
|
5261
|
+
HC_SCRIPT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
|
|
5262
|
+
HC_PROJECT_ROOT=$(CDPATH= cd -- "$HC_SCRIPT_DIR/../.." && pwd)
|
|
5263
|
+
export CLAUDE_PROJECT_DIR="$HC_PROJECT_ROOT"
|
|
5264
|
+
|
|
5265
|
+
trap - ERR
|
|
5266
|
+
set +e
|
|
5267
|
+
exec bash "$HC_PROJECT_ROOT/.claude/hooks/guard-runner.sh" hook user-prompt-findings <<<"$HC_INPUT"
|
|
5268
|
+
`;
|
|
5269
|
+
}
|
|
4515
5270
|
var CI_STANZA_KNOWN_HEADER_HASHES = [
|
|
4516
5271
|
"b225800b2d699811f32ac213238cbf81c35b5e6a74114bdf55e39cd9d7e2928f",
|
|
4517
5272
|
"674181735460f051af7c157222cf851fc3000fb3b8af3a10ea0bde9fe5eaba1f",
|
|
@@ -4537,16 +5292,13 @@ function isTrackedAndClean(targetRepo, relPath) {
|
|
|
4537
5292
|
}
|
|
4538
5293
|
return true;
|
|
4539
5294
|
}
|
|
4540
|
-
function
|
|
4541
|
-
|
|
4542
|
-
|
|
4543
|
-
function removeRetiredGeneratedFile(targetRepo, rel, headerLineIndex, knownHeaderHashes) {
|
|
4544
|
-
const path = join5(targetRepo, rel);
|
|
4545
|
-
if (!existsSync3(path))
|
|
5295
|
+
function decideRetiredRemoval(targetRepo, rel, headerLineIndex, knownHeaderHashes) {
|
|
5296
|
+
const path = join6(targetRepo, rel);
|
|
5297
|
+
if (!existsSync4(path))
|
|
4546
5298
|
return "absent";
|
|
4547
5299
|
let raw;
|
|
4548
5300
|
try {
|
|
4549
|
-
raw =
|
|
5301
|
+
raw = readFileSync5(path, "utf-8");
|
|
4550
5302
|
} catch {
|
|
4551
5303
|
return "failed";
|
|
4552
5304
|
}
|
|
@@ -4556,13 +5308,20 @@ function removeRetiredGeneratedFile(targetRepo, rel, headerLineIndex, knownHeade
|
|
|
4556
5308
|
return "kept-foreign";
|
|
4557
5309
|
if (!isTrackedAndClean(targetRepo, rel))
|
|
4558
5310
|
return "kept-uncommitted";
|
|
5311
|
+
return "removed";
|
|
5312
|
+
}
|
|
5313
|
+
function removePlannedRetiredFile(targetRepo, rel) {
|
|
4559
5314
|
try {
|
|
4560
|
-
rmSync2(
|
|
5315
|
+
rmSync2(join6(targetRepo, rel));
|
|
4561
5316
|
} catch {
|
|
4562
5317
|
return "failed";
|
|
4563
5318
|
}
|
|
4564
5319
|
return "removed";
|
|
4565
5320
|
}
|
|
5321
|
+
var LEGACY_CI_STANZA_REL = ".halfcycle/ci-stanza.yml";
|
|
5322
|
+
function planLegacyCiStanzaRemoval(targetRepo) {
|
|
5323
|
+
return decideRetiredRemoval(targetRepo, LEGACY_CI_STANZA_REL, 0, CI_STANZA_KNOWN_HEADER_HASHES);
|
|
5324
|
+
}
|
|
4566
5325
|
var MCP_HEADERS_HELPER_COMMAND = `node -e "${nodeFloorProgram([
|
|
4567
5326
|
`const p=require('path'),f=require('fs'),c=require('child_process');`,
|
|
4568
5327
|
`let d=process.cwd();`,
|
|
@@ -4581,13 +5340,14 @@ var MCP_HEADERS_KNOWN_HEADER_HASHES = [
|
|
|
4581
5340
|
"37a733ea588b3361b2bbfd86c44e0c95da30d7af6acffd1243ece83c811f0af2",
|
|
4582
5341
|
"6732ec6d2d2bf7cb33017749a3d021b59af653dcd07c3811693b5a242d44b1a1"
|
|
4583
5342
|
];
|
|
4584
|
-
function
|
|
4585
|
-
return
|
|
5343
|
+
function planMcpHeadersHelperRemoval(targetRepo) {
|
|
5344
|
+
return decideRetiredRemoval(targetRepo, RETIRED_MCP_HEADERS_HELPER_REL, 1, MCP_HEADERS_KNOWN_HEADER_HASHES);
|
|
4586
5345
|
}
|
|
4587
5346
|
var OWNED_GENERATED_HEADERS = {
|
|
4588
5347
|
".claude/hooks/guard-runner.sh": GENERATED_GUARD_RUNNER_HEADER,
|
|
4589
5348
|
".claude/hooks/user-prompt-reminder.sh": GENERATED_USER_PROMPT_REMINDER_HEADER,
|
|
4590
5349
|
".claude/hooks/user-prompt-scope.sh": GENERATED_USER_PROMPT_SCOPE_HEADER,
|
|
5350
|
+
".claude/hooks/user-prompt-findings.sh": GENERATED_USER_PROMPT_FINDINGS_HEADER,
|
|
4591
5351
|
".claude/hooks/session-end-marker.sh": GENERATED_SESSION_END_HEADER,
|
|
4592
5352
|
".claude/hooks/session-start-marker.sh": GENERATED_SESSION_START_HEADER
|
|
4593
5353
|
};
|
|
@@ -4597,6 +5357,7 @@ function hookScriptsThisVersionWrites() {
|
|
|
4597
5357
|
[".claude/hooks/session-end-marker.sh", generateSessionEndMarker()],
|
|
4598
5358
|
[".claude/hooks/user-prompt-reminder.sh", generateUserPromptReminderHook()],
|
|
4599
5359
|
[".claude/hooks/user-prompt-scope.sh", generateUserPromptScopeHook()],
|
|
5360
|
+
[".claude/hooks/user-prompt-findings.sh", generateUserPromptFindingsHook()],
|
|
4600
5361
|
[".claude/hooks/session-start-marker.sh", generateSessionStartMarker()]
|
|
4601
5362
|
];
|
|
4602
5363
|
}
|
|
@@ -4650,23 +5411,26 @@ function gitignoreCovers(content) {
|
|
|
4650
5411
|
const lines = new Set(content.split("\n").map((l) => l.trim()));
|
|
4651
5412
|
return REQUIRED_GITIGNORE_ENTRIES.every((entry) => lines.has(entry));
|
|
4652
5413
|
}
|
|
4653
|
-
function
|
|
4654
|
-
const gitignorePath =
|
|
5414
|
+
function planGitignore(targetRepo) {
|
|
5415
|
+
const gitignorePath = join6(targetRepo, GITIGNORE_REL);
|
|
4655
5416
|
const header = GITIGNORE_HEADER;
|
|
5417
|
+
const before = existsSync4(gitignorePath) ? readFileSync5(gitignorePath, "utf-8") : null;
|
|
5418
|
+
if (before === null) {
|
|
5419
|
+
return { before, after: [header, ...REQUIRED_GITIGNORE_ENTRIES].join("\n") + "\n", outcome: "written" };
|
|
5420
|
+
}
|
|
5421
|
+
if (gitignoreCovers(before))
|
|
5422
|
+
return { before, after: before, outcome: "skipped" };
|
|
5423
|
+
const existingLines = new Set(before.split("\n").map((l) => l.trim()));
|
|
5424
|
+
const toAdd = REQUIRED_GITIGNORE_ENTRIES.filter((entry) => !existingLines.has(entry));
|
|
5425
|
+
const eol = lineEndingOf(before);
|
|
5426
|
+
const suffix = (before.endsWith("\n") ? "" : eol) + eol + [header, ...toAdd].join(eol) + eol;
|
|
5427
|
+
return { before, after: before + suffix, outcome: "written" };
|
|
5428
|
+
}
|
|
5429
|
+
function putPlannedGitignore(targetRepo, planned) {
|
|
5430
|
+
if (planned.outcome !== "written" || planned.after === null)
|
|
5431
|
+
return planned.outcome;
|
|
4656
5432
|
try {
|
|
4657
|
-
|
|
4658
|
-
const body = [header, ...REQUIRED_GITIGNORE_ENTRIES].join("\n") + "\n";
|
|
4659
|
-
writeFileSync4(gitignorePath, body, "utf-8");
|
|
4660
|
-
return "written";
|
|
4661
|
-
}
|
|
4662
|
-
const existing = readFileSync4(gitignorePath, "utf-8");
|
|
4663
|
-
if (gitignoreCovers(existing))
|
|
4664
|
-
return "skipped";
|
|
4665
|
-
const existingLines = new Set(existing.split("\n").map((l) => l.trim()));
|
|
4666
|
-
const toAdd = REQUIRED_GITIGNORE_ENTRIES.filter((entry) => !existingLines.has(entry));
|
|
4667
|
-
const eol = lineEndingOf(existing);
|
|
4668
|
-
const suffix = (existing.endsWith("\n") ? "" : eol) + eol + [header, ...toAdd].join(eol) + eol;
|
|
4669
|
-
writeFileSync4(gitignorePath, existing + suffix, "utf-8");
|
|
5433
|
+
writeFileSync4(join6(targetRepo, GITIGNORE_REL), planned.after, "utf-8");
|
|
4670
5434
|
return "written";
|
|
4671
5435
|
} catch {
|
|
4672
5436
|
return "failed";
|
|
@@ -4675,7 +5439,7 @@ function reconcileGitignore(targetRepo) {
|
|
|
4675
5439
|
var GITATTRIBUTES_HEADER = "# Halfcycle \u2014 generated scripts keep LF line endings";
|
|
4676
5440
|
function readTextOrNull(path) {
|
|
4677
5441
|
try {
|
|
4678
|
-
return
|
|
5442
|
+
return readFileSync5(path, "utf-8");
|
|
4679
5443
|
} catch {
|
|
4680
5444
|
return null;
|
|
4681
5445
|
}
|
|
@@ -4684,26 +5448,31 @@ function lfRules(paths) {
|
|
|
4684
5448
|
return [...paths].filter((path) => path.endsWith(".sh")).map((path) => `${path} text eol=lf`);
|
|
4685
5449
|
}
|
|
4686
5450
|
var REQUIRED_GITATTRIBUTES_RULES = lfRules(Object.keys(OWNED_GENERATED_HEADERS));
|
|
4687
|
-
function
|
|
4688
|
-
const path =
|
|
5451
|
+
function planGitattributes(targetRepo) {
|
|
5452
|
+
const path = join6(targetRepo, GITATTRIBUTES_REL);
|
|
4689
5453
|
const rules = REQUIRED_GITATTRIBUTES_RULES;
|
|
4690
|
-
const
|
|
5454
|
+
const before = readTextOrNull(path);
|
|
5455
|
+
if (lstatOrNull(path)?.isSymbolicLink() === true)
|
|
5456
|
+
return { before, after: before, outcome: "failed" };
|
|
5457
|
+
if (!existsSync4(path)) {
|
|
5458
|
+
return { before, after: [GITATTRIBUTES_HEADER, ...rules].join("\n") + "\n", outcome: "written" };
|
|
5459
|
+
}
|
|
5460
|
+
if (before === null)
|
|
5461
|
+
return { before, after: null, outcome: "failed" };
|
|
5462
|
+
const present = new Set(before.split("\n").map((l) => l.trim()));
|
|
5463
|
+
const toAdd = rules.filter((rule) => !present.has(rule));
|
|
5464
|
+
if (toAdd.length === 0)
|
|
5465
|
+
return { before, after: before, outcome: "skipped" };
|
|
5466
|
+
const eol = lineEndingOf(before);
|
|
5467
|
+
const separator = before === "" ? "" : before.endsWith("\n") ? eol : eol + eol;
|
|
5468
|
+
const suffix = separator + [GITATTRIBUTES_HEADER, ...toAdd].join(eol) + eol;
|
|
5469
|
+
return { before, after: before + suffix, outcome: "written" };
|
|
5470
|
+
}
|
|
5471
|
+
function putPlannedGitattributes(targetRepo, planned) {
|
|
5472
|
+
if (planned.outcome !== "written" || planned.after === null)
|
|
5473
|
+
return planned.outcome;
|
|
4691
5474
|
try {
|
|
4692
|
-
|
|
4693
|
-
return "failed";
|
|
4694
|
-
if (!existsSync3(path)) {
|
|
4695
|
-
writeAllowlisted(path, targetRepo, [GITATTRIBUTES_HEADER, ...rules].join("\n") + "\n", routed);
|
|
4696
|
-
return "written";
|
|
4697
|
-
}
|
|
4698
|
-
const existing = readFileSync4(path, "utf-8");
|
|
4699
|
-
const present = new Set(existing.split("\n").map((l) => l.trim()));
|
|
4700
|
-
const toAdd = rules.filter((rule) => !present.has(rule));
|
|
4701
|
-
if (toAdd.length === 0)
|
|
4702
|
-
return "skipped";
|
|
4703
|
-
const eol = lineEndingOf(existing);
|
|
4704
|
-
const separator = existing === "" ? "" : existing.endsWith("\n") ? eol : eol + eol;
|
|
4705
|
-
const suffix = separator + [GITATTRIBUTES_HEADER, ...toAdd].join(eol) + eol;
|
|
4706
|
-
writeAllowlisted(path, targetRepo, existing + suffix, routed);
|
|
5475
|
+
writeAllowlisted(join6(targetRepo, GITATTRIBUTES_REL), targetRepo, planned.after, []);
|
|
4707
5476
|
return "written";
|
|
4708
5477
|
} catch {
|
|
4709
5478
|
return "failed";
|
|
@@ -4722,7 +5491,7 @@ function previousPinForEngagement(targetRepoRoot, engagementId) {
|
|
|
4722
5491
|
function nonEmpty(value) {
|
|
4723
5492
|
return typeof value === "string" && value.trim() !== "" ? value : void 0;
|
|
4724
5493
|
}
|
|
4725
|
-
function
|
|
5494
|
+
function planBundlePin(targetRepoRoot, version, engagementId, engagementType, createdByAccountId, methodVersion) {
|
|
4726
5495
|
const previous = previousPinForEngagement(targetRepoRoot, engagementId);
|
|
4727
5496
|
const carried = pinCreatedByAccountId(previous) ?? nonEmpty(createdByAccountId);
|
|
4728
5497
|
const carriedMethodVersion = nonEmpty(methodVersion) ?? nonEmpty(previous?.methodVersion);
|
|
@@ -4737,9 +5506,39 @@ function writeBundlePin(targetRepoRoot, version, engagementId, engagementType, c
|
|
|
4737
5506
|
...carriedMethodVersion !== void 0 ? { methodVersion: carriedMethodVersion } : {},
|
|
4738
5507
|
...projectPage !== void 0 ? { projectPage } : {}
|
|
4739
5508
|
};
|
|
4740
|
-
const
|
|
4741
|
-
const
|
|
4742
|
-
|
|
5509
|
+
const current = readFileOrNull(join6(targetRepoRoot, BUNDLE_PIN_REL));
|
|
5510
|
+
const text = inLineEndingOf(current, JSON.stringify(pin, null, 2) + "\n");
|
|
5511
|
+
return {
|
|
5512
|
+
rel: BUNDLE_PIN_REL,
|
|
5513
|
+
text,
|
|
5514
|
+
outcome: current === text ? "skipped" : "written",
|
|
5515
|
+
replacedExisting: current !== null && current !== text
|
|
5516
|
+
};
|
|
5517
|
+
}
|
|
5518
|
+
function planBundlePinPreview(targetRepoRoot, version, home) {
|
|
5519
|
+
let existing;
|
|
5520
|
+
try {
|
|
5521
|
+
existing = readBundlePin(targetRepoRoot);
|
|
5522
|
+
} catch {
|
|
5523
|
+
return null;
|
|
5524
|
+
}
|
|
5525
|
+
if (existing === null || nonEmpty(existing.engagementId) === void 0 || existing.engagementType === void 0) {
|
|
5526
|
+
return null;
|
|
5527
|
+
}
|
|
5528
|
+
let pinned;
|
|
5529
|
+
try {
|
|
5530
|
+
pinned = readPinnedEngagement(targetRepoRoot, home);
|
|
5531
|
+
} catch {
|
|
5532
|
+
return null;
|
|
5533
|
+
}
|
|
5534
|
+
if (pinned === null || pinned.engagementId !== existing.engagementId || pinned.credential === void 0) {
|
|
5535
|
+
return null;
|
|
5536
|
+
}
|
|
5537
|
+
return planBundlePin(targetRepoRoot, version, existing.engagementId, existing.engagementType, void 0, void 0);
|
|
5538
|
+
}
|
|
5539
|
+
function writeBundlePin(targetRepoRoot, planned, writtenPaths) {
|
|
5540
|
+
const pinPath = join6(targetRepoRoot, BUNDLE_PIN_REL);
|
|
5541
|
+
writeAllowlisted(pinPath, targetRepoRoot, planned.text, writtenPaths);
|
|
4743
5542
|
}
|
|
4744
5543
|
function renderCrewRoster() {
|
|
4745
5544
|
const doc = {
|
|
@@ -4749,19 +5548,40 @@ function renderCrewRoster() {
|
|
|
4749
5548
|
return `${JSON.stringify(doc, null, 2)}
|
|
4750
5549
|
`;
|
|
4751
5550
|
}
|
|
4752
|
-
function writeCrewRoster(targetRepoRoot, report) {
|
|
4753
|
-
const rendered = renderCrewRoster();
|
|
4754
|
-
const crewPath = join5(targetRepoRoot, CREW_ROSTER_REL);
|
|
4755
|
-
return recordOwned(report, crewPath, targetRepoRoot, rendered, CREW_ROSTER_REL);
|
|
4756
|
-
}
|
|
4757
5551
|
function pinCreatedByAccountId(pin) {
|
|
4758
5552
|
return nonEmpty(pin?.createdByAccountId) ?? nonEmpty(pin?.accountId);
|
|
4759
5553
|
}
|
|
4760
5554
|
function readBundlePin(targetRepoRoot) {
|
|
4761
|
-
const pinPath =
|
|
4762
|
-
if (!
|
|
5555
|
+
const pinPath = join6(targetRepoRoot, BUNDLE_PIN_REL);
|
|
5556
|
+
if (!existsSync4(pinPath))
|
|
5557
|
+
return null;
|
|
5558
|
+
return JSON.parse(readFileSync5(pinPath, "utf-8"));
|
|
5559
|
+
}
|
|
5560
|
+
function readPinnedEngagement(targetRepoRoot, home) {
|
|
5561
|
+
const pin = readBundlePin(targetRepoRoot);
|
|
5562
|
+
const engagementId = pin?.engagementId;
|
|
5563
|
+
if (engagementId === void 0 || engagementId === "")
|
|
5564
|
+
return null;
|
|
5565
|
+
assertEngagementId(engagementId);
|
|
5566
|
+
const stored = readEngagementEnv(engagementId, home);
|
|
5567
|
+
const fromStore = stored === null ? void 0 : toCredential(stored);
|
|
5568
|
+
if (fromStore !== void 0) {
|
|
5569
|
+
return { engagementId, credential: fromStore, fromLegacyEnvLocal: false };
|
|
5570
|
+
}
|
|
5571
|
+
const legacy = readLegacyEnvLocal(targetRepoRoot);
|
|
5572
|
+
const fromLegacy = legacy === null ? void 0 : toCredential(legacy);
|
|
5573
|
+
return {
|
|
5574
|
+
engagementId,
|
|
5575
|
+
credential: fromLegacy,
|
|
5576
|
+
fromLegacyEnvLocal: fromLegacy !== void 0
|
|
5577
|
+
};
|
|
5578
|
+
}
|
|
5579
|
+
function readLegacyEnvLocal(targetRepoRoot) {
|
|
5580
|
+
try {
|
|
5581
|
+
return parseEnvText(readFileSync5(join6(targetRepoRoot, LEGACY_ENV_LOCAL_REL), "utf-8"));
|
|
5582
|
+
} catch {
|
|
4763
5583
|
return null;
|
|
4764
|
-
|
|
5584
|
+
}
|
|
4765
5585
|
}
|
|
4766
5586
|
var CREDENTIAL_REQUIRED_KEYS = [
|
|
4767
5587
|
"HALFCYCLE_SERVICE_URL",
|
|
@@ -4774,6 +5594,18 @@ function missingRequiredKeys(found) {
|
|
|
4774
5594
|
const resolved = { ...found, HALFCYCLE_SERVICE_URL: serviceUrl };
|
|
4775
5595
|
return CREDENTIAL_REQUIRED_KEYS.filter((key) => (resolved[key] ?? "") === "");
|
|
4776
5596
|
}
|
|
5597
|
+
function toCredential(found) {
|
|
5598
|
+
if (missingRequiredKeys(found).length > 0)
|
|
5599
|
+
return void 0;
|
|
5600
|
+
const controlTelemetryUrl = found["CONTROL_TELEMETRY_URL"];
|
|
5601
|
+
return {
|
|
5602
|
+
serviceUrl: (process.env["HALFCYCLE_SERVICE_URL"]?.trim() ?? "") || (found["HALFCYCLE_SERVICE_URL"] ?? ""),
|
|
5603
|
+
token: found["HALFCYCLE_TOKEN"] ?? "",
|
|
5604
|
+
mcpUrl: found["HALFCYCLE_MCP_URL"] ?? "",
|
|
5605
|
+
guardUrl: found["GUARD_SERVICE_URL"] ?? "",
|
|
5606
|
+
...controlTelemetryUrl ? { controlTelemetryUrl } : {}
|
|
5607
|
+
};
|
|
5608
|
+
}
|
|
4777
5609
|
function checkDrift(targetRepoRoot) {
|
|
4778
5610
|
const manifest = readPluginManifest();
|
|
4779
5611
|
const pin = readBundlePin(targetRepoRoot);
|
|
@@ -4781,15 +5613,32 @@ function checkDrift(targetRepoRoot) {
|
|
|
4781
5613
|
const current = manifest.version;
|
|
4782
5614
|
return { drifted: installed !== current, installed, current, methodVersion: pin?.methodVersion ?? null };
|
|
4783
5615
|
}
|
|
4784
|
-
function writeProjectIdentity(targetRepo) {
|
|
4785
|
-
if (
|
|
4786
|
-
|
|
4787
|
-
return "skipped";
|
|
5616
|
+
function writeProjectIdentity(targetRepo, planned, probe = false) {
|
|
5617
|
+
if (planned.action === "add" || probe) {
|
|
5618
|
+
createProjectIdentityFile(targetRepo, planned.after ?? void 0);
|
|
4788
5619
|
}
|
|
4789
|
-
|
|
4790
|
-
|
|
5620
|
+
if (planned.action === "change" && planned.after !== null)
|
|
5621
|
+
replaceProjectIdentityFile(targetRepo, planned.after);
|
|
5622
|
+
return planned.action === "add" ? "written" : "skipped";
|
|
4791
5623
|
}
|
|
4792
5624
|
function writeEngagementCredential(credential, engagementId, home) {
|
|
5625
|
+
return writeEngagementEnv(engagementId, engagementCredentialValues(credential, engagementId), home);
|
|
5626
|
+
}
|
|
5627
|
+
function planEngagementCredential(credential, engagementId, home) {
|
|
5628
|
+
let existing;
|
|
5629
|
+
try {
|
|
5630
|
+
existing = readFileSync5(engagementEnvPath(engagementId, home), "utf-8");
|
|
5631
|
+
} catch {
|
|
5632
|
+
existing = null;
|
|
5633
|
+
}
|
|
5634
|
+
const reconciled = reconcileEnvText2(existing, engagementCredentialValues(credential, engagementId));
|
|
5635
|
+
return {
|
|
5636
|
+
outcome: existing === reconciled ? "skipped" : "written",
|
|
5637
|
+
existed: existing !== null,
|
|
5638
|
+
storedAfter: parseEnvText(reconciled)
|
|
5639
|
+
};
|
|
5640
|
+
}
|
|
5641
|
+
function engagementCredentialValues(credential, engagementId) {
|
|
4793
5642
|
const values = {
|
|
4794
5643
|
HALFCYCLE_SERVICE_URL: credential.serviceUrl,
|
|
4795
5644
|
// T-22 — the second origin, persisted for the developer and for any tool that
|
|
@@ -4817,16 +5666,16 @@ function writeEngagementCredential(credential, engagementId, home) {
|
|
|
4817
5666
|
// telemetry origin, same as every other optional field on `DeviceCredential`.
|
|
4818
5667
|
CONTROL_TELEMETRY_URL: credential.controlTelemetryUrl ?? ""
|
|
4819
5668
|
};
|
|
4820
|
-
return
|
|
5669
|
+
return values;
|
|
4821
5670
|
}
|
|
4822
5671
|
var LEGACY_ENV_LOCAL_REL = ".env.local";
|
|
4823
5672
|
function migrateLegacyEnvLocal(targetRepo, stored) {
|
|
4824
|
-
const path =
|
|
4825
|
-
if (!
|
|
5673
|
+
const path = join6(targetRepo, LEGACY_ENV_LOCAL_REL);
|
|
5674
|
+
if (!existsSync4(path))
|
|
4826
5675
|
return { outcome: "absent", unbacked: [], missingForAdoption: [] };
|
|
4827
5676
|
let raw;
|
|
4828
5677
|
try {
|
|
4829
|
-
raw =
|
|
5678
|
+
raw = readFileSync5(path, "utf-8");
|
|
4830
5679
|
} catch {
|
|
4831
5680
|
return { outcome: "failed", unbacked: [], missingForAdoption: [] };
|
|
4832
5681
|
}
|
|
@@ -4847,24 +5696,91 @@ function migrateLegacyEnvLocal(targetRepo, stored) {
|
|
|
4847
5696
|
return !ours.has(line.slice(0, eq).replace(/^\s*export\s+/, "").trim());
|
|
4848
5697
|
});
|
|
4849
5698
|
const substantive = kept.filter((line) => line.trim() !== "" && !line.trim().startsWith("# Halfcycle"));
|
|
5699
|
+
if (substantive.length === 0)
|
|
5700
|
+
return { outcome: "removed", unbacked: [], missingForAdoption: [] };
|
|
5701
|
+
return { outcome: "stripped", unbacked: [], missingForAdoption: [], keptText: kept.join("\n") };
|
|
5702
|
+
}
|
|
5703
|
+
function applyLegacyMigration(targetRepo, decided) {
|
|
5704
|
+
const path = join6(targetRepo, LEGACY_ENV_LOCAL_REL);
|
|
4850
5705
|
try {
|
|
4851
|
-
if (
|
|
5706
|
+
if (decided.outcome === "removed") {
|
|
4852
5707
|
rmSync2(path);
|
|
4853
|
-
return
|
|
5708
|
+
return decided;
|
|
5709
|
+
}
|
|
5710
|
+
if (decided.outcome === "stripped" && decided.keptText !== void 0) {
|
|
5711
|
+
writeFileSync4(path, decided.keptText, "utf-8");
|
|
5712
|
+
return decided;
|
|
4854
5713
|
}
|
|
4855
|
-
|
|
4856
|
-
return { outcome: "stripped", unbacked: [], missingForAdoption: [] };
|
|
5714
|
+
return decided;
|
|
4857
5715
|
} catch {
|
|
4858
5716
|
return { outcome: "failed", unbacked: [], missingForAdoption: [] };
|
|
4859
5717
|
}
|
|
4860
5718
|
}
|
|
4861
5719
|
async function install(options) {
|
|
4862
|
-
|
|
4863
|
-
|
|
4864
|
-
|
|
4865
|
-
throw new Error(`[halfcycle] Target repo does not exist: ${targetRepo}`);
|
|
4866
|
-
}
|
|
5720
|
+
return applyInstallPlan(planInstall(options));
|
|
5721
|
+
}
|
|
5722
|
+
function planCommandStubs(targetRepo, previous, writeSet) {
|
|
4867
5723
|
const manifest = readPluginManifest();
|
|
5724
|
+
return { version: manifest.version, commands: planManifestCommands(manifest, targetRepo, previous, writeSet) };
|
|
5725
|
+
}
|
|
5726
|
+
function planVendoredBinary(targetRepo) {
|
|
5727
|
+
return decideOwned(join6(targetRepo, ".halfcycle", "bin", "bin.bundle.mjs"), targetRepo, readFileSync5(resolveVendoredBinary(), "utf-8"));
|
|
5728
|
+
}
|
|
5729
|
+
function planSettings(targetRepo, previous, home, env) {
|
|
5730
|
+
const settingsPath = join6(targetRepo, SETTINGS_REL);
|
|
5731
|
+
const preexisted = existsSync4(settingsPath);
|
|
5732
|
+
const generated = JSON.parse(generateSettingsJson());
|
|
5733
|
+
let existing;
|
|
5734
|
+
try {
|
|
5735
|
+
existing = readExistingSettings(targetRepo) ?? {};
|
|
5736
|
+
} catch (err) {
|
|
5737
|
+
throw new SettingsParseRefused(settingsParseRefusal(targetRepo, err));
|
|
5738
|
+
}
|
|
5739
|
+
const retiredOurs = retiredRulesThisInstallAdded(previous);
|
|
5740
|
+
const statusLine = statusLineSetElsewhere(targetRepo, home, env) ? void 0 : HALFCYCLE_STATUS_LINE;
|
|
5741
|
+
const merged = mergeSettings(existing, generated, retiredOurs, statusLine);
|
|
5742
|
+
const current = readFileOrNull(settingsPath);
|
|
5743
|
+
const text = inLineEndingOf(current, JSON.stringify(merged, null, 2) + "\n");
|
|
5744
|
+
const file = {
|
|
5745
|
+
rel: SETTINGS_REL,
|
|
5746
|
+
text,
|
|
5747
|
+
// Identical bytes are not rewritten; the report still names the file as merged or
|
|
5748
|
+
// written by whether it was there, as it always has.
|
|
5749
|
+
outcome: current === text ? "skipped" : "written",
|
|
5750
|
+
replacedExisting: false
|
|
5751
|
+
};
|
|
5752
|
+
return { file, preexisted, existing, generated, merged, retiredOurs };
|
|
5753
|
+
}
|
|
5754
|
+
function planHookScripts(targetRepo) {
|
|
5755
|
+
return hookScriptsThisVersionWrites().map(([rel, content]) => decideOwnedGenerated(join6(targetRepo, rel), targetRepo, content, OWNED_GENERATED_HEADERS[rel]));
|
|
5756
|
+
}
|
|
5757
|
+
function planMcpRegistration(targetRepo, credential) {
|
|
5758
|
+
const mcpPath = join6(targetRepo, MCP_REGISTRATION_REL);
|
|
5759
|
+
const existing = existsSync4(mcpPath) ? readFileSync5(mcpPath, "utf-8") : null;
|
|
5760
|
+
if (credential === void 0)
|
|
5761
|
+
return { kind: "none", existing };
|
|
5762
|
+
if (credential === "pending")
|
|
5763
|
+
return { kind: "pending", existing };
|
|
5764
|
+
const text = inLineEndingOf(existing, generateMcpRegistration(existing, credential.mcpUrl));
|
|
5765
|
+
const outcome = existing === null ? "written" : existing !== text ? "merged" : "skipped";
|
|
5766
|
+
return { kind: "write", existing, text, outcome };
|
|
5767
|
+
}
|
|
5768
|
+
function putPlannedMcp(targetRepo, planned) {
|
|
5769
|
+
if (planned.kind !== "write" || planned.outcome === "skipped")
|
|
5770
|
+
return;
|
|
5771
|
+
writeAllowlisted(join6(targetRepo, MCP_REGISTRATION_REL), targetRepo, planned.text, []);
|
|
5772
|
+
}
|
|
5773
|
+
function planCrewRoster(targetRepo) {
|
|
5774
|
+
return decideOwned(join6(targetRepo, CREW_ROSTER_REL), targetRepo, renderCrewRoster());
|
|
5775
|
+
}
|
|
5776
|
+
function planInstallRecord(targetRepo, text) {
|
|
5777
|
+
return decideOwned(join6(targetRepo, INSTALL_MANIFEST_REL), targetRepo, text);
|
|
5778
|
+
}
|
|
5779
|
+
function accountInstall(steps, actual) {
|
|
5780
|
+
const { targetRepo, writeSet, pin } = steps;
|
|
5781
|
+
if (pin === null || steps.mcp.kind === "pending" || steps.engagementId === void 0) {
|
|
5782
|
+
throw new Error("[halfcycle] a plan still waiting for the service cannot be applied or recorded.");
|
|
5783
|
+
}
|
|
4868
5784
|
const report = {
|
|
4869
5785
|
writtenPaths: [],
|
|
4870
5786
|
skippedPaths: [],
|
|
@@ -4873,38 +5789,22 @@ async function install(options) {
|
|
|
4873
5789
|
editedCollidedPaths: [],
|
|
4874
5790
|
replacedPaths: []
|
|
4875
5791
|
};
|
|
4876
|
-
const
|
|
4877
|
-
|
|
4878
|
-
|
|
4879
|
-
|
|
4880
|
-
|
|
4881
|
-
|
|
4882
|
-
|
|
4883
|
-
noteFile(draft,
|
|
4884
|
-
const
|
|
4885
|
-
|
|
4886
|
-
|
|
4887
|
-
|
|
4888
|
-
|
|
4889
|
-
|
|
4890
|
-
}
|
|
4891
|
-
|
|
4892
|
-
}
|
|
4893
|
-
const retiredOurs = retiredRulesThisInstallAdded(draft.previous);
|
|
4894
|
-
const statusLine = statusLineSetElsewhere(targetRepo, home, options.env) ? void 0 : HALFCYCLE_STATUS_LINE;
|
|
4895
|
-
const mergedSettings = mergeSettings(existingSettings, generatedSettings, retiredOurs, statusLine);
|
|
4896
|
-
recordSettingsMerge(draft, existingSettings, generatedSettings, settingsPreexisted, mergedSettings);
|
|
4897
|
-
recordDenyRetired(draft, retiredOurs.map((rule) => sha256Hex(rule)));
|
|
4898
|
-
const mergedSettingsText = inLineEndingOf(readFileOrNull(settingsPath), JSON.stringify(mergedSettings, null, 2) + "\n");
|
|
4899
|
-
mkdirSync4(dirname3(settingsPath), { recursive: true });
|
|
4900
|
-
writeFileSync4(settingsPath, mergedSettingsText, "utf-8");
|
|
4901
|
-
(settingsPreexisted ? report.mergedPaths : report.writtenPaths).push(".claude/settings.json");
|
|
4902
|
-
for (const [rel, content] of hookScriptsThisVersionWrites()) {
|
|
4903
|
-
const hookPath = join5(targetRepo, rel);
|
|
4904
|
-
const outcome = recordOwnedGenerated(report, hookPath, targetRepo, content, OWNED_GENERATED_HEADERS[rel], rel);
|
|
4905
|
-
noteFile(draft, targetRepo, rel, outcome);
|
|
4906
|
-
}
|
|
4907
|
-
switch (removeLegacyCiStanza(targetRepo)) {
|
|
5792
|
+
const draft = startManifest(steps.previous, (path) => writeSet.files.has(path), (path) => writeSet.dirs.has(path));
|
|
5793
|
+
for (const command of steps.commands) {
|
|
5794
|
+
recordOwnedGenerated(report, command);
|
|
5795
|
+
if (command.editedCollided)
|
|
5796
|
+
report.editedCollidedPaths.push(command.rel);
|
|
5797
|
+
noteFile(draft, command.rel, command.outcome, actual.bytesAfter);
|
|
5798
|
+
}
|
|
5799
|
+
noteFile(draft, VENDORED_BIN_REL, recordOwnedGenerated(report, steps.vendored), actual.bytesAfter);
|
|
5800
|
+
const settings = steps.settings;
|
|
5801
|
+
recordSettingsMerge(draft, settings.existing, settings.generated, settings.preexisted, settings.merged);
|
|
5802
|
+
recordDenyRetired(draft, settings.retiredOurs.map((rule) => sha256Hex(rule)));
|
|
5803
|
+
(settings.preexisted ? report.mergedPaths : report.writtenPaths).push(".claude/settings.json");
|
|
5804
|
+
for (const hook of steps.hooks) {
|
|
5805
|
+
noteFile(draft, hook.rel, recordOwnedGenerated(report, hook), actual.bytesAfter);
|
|
5806
|
+
}
|
|
5807
|
+
switch (actual.ciStanza) {
|
|
4908
5808
|
case "removed":
|
|
4909
5809
|
report.writtenPaths.push(".halfcycle/ci-stanza.yml (REMOVED \u2014 Halfcycle no longer generates this file)");
|
|
4910
5810
|
break;
|
|
@@ -4920,20 +5820,17 @@ async function install(options) {
|
|
|
4920
5820
|
case "absent":
|
|
4921
5821
|
break;
|
|
4922
5822
|
}
|
|
4923
|
-
|
|
4924
|
-
|
|
4925
|
-
|
|
4926
|
-
|
|
4927
|
-
|
|
4928
|
-
if (
|
|
4929
|
-
|
|
4930
|
-
|
|
4931
|
-
writeAllowlisted(mcpPath, targetRepo, mcpContent, report.mergedPaths);
|
|
4932
|
-
} else {
|
|
5823
|
+
const mcp = steps.mcp;
|
|
5824
|
+
if (mcp.kind === "write") {
|
|
5825
|
+
recordMcpMerge(draft, mcp.existing, mcp.text);
|
|
5826
|
+
if (mcp.outcome === "written")
|
|
5827
|
+
report.writtenPaths.push(MCP_REGISTRATION_REL);
|
|
5828
|
+
else if (mcp.outcome === "merged")
|
|
5829
|
+
report.mergedPaths.push(MCP_REGISTRATION_REL);
|
|
5830
|
+
else
|
|
4933
5831
|
report.skippedPaths.push(MCP_REGISTRATION_REL);
|
|
4934
|
-
}
|
|
4935
5832
|
const helperRel = RETIRED_MCP_HEADERS_HELPER_REL;
|
|
4936
|
-
switch (
|
|
5833
|
+
switch (actual.mcpHelper) {
|
|
4937
5834
|
case "removed":
|
|
4938
5835
|
recordRemovedFile(draft, helperRel);
|
|
4939
5836
|
report.writtenPaths.push(`${helperRel} (REMOVED \u2014 Halfcycle no longer uses this file)`);
|
|
@@ -4948,43 +5845,36 @@ async function install(options) {
|
|
|
4948
5845
|
report.skippedPaths.push(`${helperRel} (could not be removed \u2014 Halfcycle no longer uses this file, DELETE IT BY HAND)`);
|
|
4949
5846
|
break;
|
|
4950
5847
|
case "absent":
|
|
5848
|
+
case null:
|
|
4951
5849
|
break;
|
|
4952
5850
|
}
|
|
4953
5851
|
} else {
|
|
4954
5852
|
report.skippedPaths.push(`${MCP_REGISTRATION_REL} (no MCP origin \u2014 no credential supplied)`);
|
|
4955
|
-
|
|
4956
|
-
|
|
4957
|
-
recordMcpAdoptionOnly(draft, readFileSync4(mcpPath, "utf-8"));
|
|
5853
|
+
if (mcp.existing !== null)
|
|
5854
|
+
recordMcpAdoptionOnly(draft, mcp.existing);
|
|
4958
5855
|
}
|
|
4959
|
-
|
|
4960
|
-
|
|
4961
|
-
const gitignoreOutcome = reconcileGitignore(targetRepo);
|
|
4962
|
-
recordGitignoreReconcile(draft, gitignoreBefore, existsSync3(gitignorePath) ? readFileSync4(gitignorePath, "utf-8") : null, writeSet);
|
|
4963
|
-
if (gitignoreOutcome === "failed") {
|
|
5856
|
+
recordGitignoreReconcile(draft, steps.gitignore.before, actual.gitignoreAfter, writeSet);
|
|
5857
|
+
if (actual.gitignore === "failed") {
|
|
4964
5858
|
report.skippedPaths.push(".gitignore (write failed \u2014 see credential refusal)");
|
|
4965
5859
|
} else {
|
|
4966
|
-
record(report,
|
|
5860
|
+
record(report, actual.gitignore, ".gitignore");
|
|
4967
5861
|
}
|
|
4968
|
-
|
|
4969
|
-
|
|
4970
|
-
const gitattributesOutcome = reconcileGitattributes(targetRepo);
|
|
4971
|
-
recordGitattributesReconcile(draft, gitattributesBefore, readTextOrNull(gitattributesPath), writeSet);
|
|
4972
|
-
if (gitattributesOutcome === "failed") {
|
|
5862
|
+
recordGitattributesReconcile(draft, steps.gitattributes.before, actual.gitattributesAfter, writeSet);
|
|
5863
|
+
if (actual.gitattributes === "failed") {
|
|
4973
5864
|
report.skippedPaths.push(`${GITATTRIBUTES_REL} (write failed \u2014 a clone made with core.autocrlf=true may check the hook scripts out with CRLF line endings)`);
|
|
4974
5865
|
} else {
|
|
4975
|
-
record(report,
|
|
5866
|
+
record(report, actual.gitattributes, GITATTRIBUTES_REL);
|
|
4976
5867
|
}
|
|
4977
|
-
const identityOutcome =
|
|
5868
|
+
const identityOutcome = steps.identity.action === "add" ? "written" : "skipped";
|
|
4978
5869
|
record(report, identityOutcome, PROJECT_IDENTITY_REL);
|
|
4979
|
-
noteFile(draft,
|
|
4980
|
-
const credentialPath = engagementEnvPath(engagementId, home);
|
|
4981
|
-
if (credential) {
|
|
4982
|
-
record(report,
|
|
5870
|
+
noteFile(draft, PROJECT_IDENTITY_REL, identityOutcome, actual.bytesAfter);
|
|
5871
|
+
const credentialPath = engagementEnvPath(steps.engagementId, steps.home);
|
|
5872
|
+
if (actual.credential !== null) {
|
|
5873
|
+
record(report, actual.credential, credentialPath);
|
|
4983
5874
|
} else {
|
|
4984
5875
|
report.skippedPaths.push(`${credentialPath} (no credential supplied)`);
|
|
4985
5876
|
}
|
|
4986
|
-
const
|
|
4987
|
-
const migration = migrateLegacyEnvLocal(targetRepo, stored);
|
|
5877
|
+
const migration = actual.legacy;
|
|
4988
5878
|
switch (migration.outcome) {
|
|
4989
5879
|
case "removed":
|
|
4990
5880
|
report.writtenPaths.push(".env.local (REMOVED \u2014 the credential now lives outside this repository)");
|
|
@@ -5001,32 +5891,22 @@ async function install(options) {
|
|
|
5001
5891
|
case "absent":
|
|
5002
5892
|
break;
|
|
5003
5893
|
}
|
|
5004
|
-
|
|
5005
|
-
|
|
5006
|
-
noteFile(draft,
|
|
5007
|
-
|
|
5008
|
-
if (
|
|
5894
|
+
if (pin.outcome === "written")
|
|
5895
|
+
report.writtenPaths.push(BUNDLE_PIN_REL);
|
|
5896
|
+
noteFile(draft, BUNDLE_PIN_REL, "written", actual.bytesAfter);
|
|
5897
|
+
noteFile(draft, CREW_ROSTER_REL, recordOwnedGenerated(report, steps.crew), actual.bytesAfter);
|
|
5898
|
+
if (actual.stateExistsAfter)
|
|
5009
5899
|
recordPerMachineFile(draft, BOOTSTRAP_STATE_REL);
|
|
5010
5900
|
for (const dir of writeSet.dirs) {
|
|
5011
|
-
if (!dirsBefore.has(dir) &&
|
|
5901
|
+
if (!steps.dirsBefore.has(dir) && actual.dirExistsAfter(dir))
|
|
5012
5902
|
recordCreatedDir(draft, dir);
|
|
5013
5903
|
}
|
|
5014
5904
|
const installManifest = finishManifest(draft);
|
|
5015
5905
|
assertManifestInWriteSet(installManifest, writeSet);
|
|
5016
|
-
|
|
5017
|
-
|
|
5018
|
-
|
|
5019
|
-
|
|
5020
|
-
writtenPaths: report.writtenPaths,
|
|
5021
|
-
skippedPaths: report.skippedPaths,
|
|
5022
|
-
mergedPaths: report.mergedPaths,
|
|
5023
|
-
collidedPaths: report.collidedPaths,
|
|
5024
|
-
editedCollidedPaths: report.editedCollidedPaths,
|
|
5025
|
-
replacedPaths: report.replacedPaths,
|
|
5026
|
-
bootstrapScanRan: scanResult.ran,
|
|
5027
|
-
legacyCredentialKept: migration.outcome === "kept" ? { keys: migration.unbacked, missing: migration.missingForAdoption } : null,
|
|
5028
|
-
notShared
|
|
5029
|
-
};
|
|
5906
|
+
return { report, installManifest, manifestText: serializeManifest(installManifest) };
|
|
5907
|
+
}
|
|
5908
|
+
function recordInstallRecord(report, planned) {
|
|
5909
|
+
recordOwnedGenerated(report, planned);
|
|
5030
5910
|
}
|
|
5031
5911
|
|
|
5032
5912
|
// dist/control-origin.js
|
|
@@ -5185,8 +6065,8 @@ async function requestEngagementValues(method, url, requestBody, credential, sha
|
|
|
5185
6065
|
}
|
|
5186
6066
|
|
|
5187
6067
|
// dist/setup/manifest.js
|
|
5188
|
-
import { existsSync as
|
|
5189
|
-
import { dirname as dirname4, join as
|
|
6068
|
+
import { existsSync as existsSync5, lstatSync as lstatSync2, mkdirSync as mkdirSync5, readFileSync as readFileSync6, statSync, writeFileSync as writeFileSync5 } from "node:fs";
|
|
6069
|
+
import { dirname as dirname4, join as join7, posix as posix3 } from "node:path";
|
|
5190
6070
|
var DECLARED_ROOT_FILES = ["CLAUDE.md", "AGENTS.md", "ENGAGEMENT.md"];
|
|
5191
6071
|
var DECLARED_DIR_PREFIX = "docs";
|
|
5192
6072
|
var RESERVED_METHOD_PATH = "docs/method";
|
|
@@ -5197,7 +6077,7 @@ function normalise(path) {
|
|
|
5197
6077
|
const slashed = path.replace(/\\/g, "/");
|
|
5198
6078
|
if (slashed.trim() === "")
|
|
5199
6079
|
return "";
|
|
5200
|
-
return
|
|
6080
|
+
return posix3.normalize(slashed).replace(/\/+$/, "");
|
|
5201
6081
|
}
|
|
5202
6082
|
function isUnder(candidate, prefix) {
|
|
5203
6083
|
return candidate === prefix || candidate.startsWith(prefix + "/");
|
|
@@ -5271,7 +6151,7 @@ function firstSymlinkOnPath(targetRepo, path) {
|
|
|
5271
6151
|
const segments = path.split("/").filter((s) => s.length > 0);
|
|
5272
6152
|
let walked = targetRepo;
|
|
5273
6153
|
for (let i = 0; i < segments.length; i++) {
|
|
5274
|
-
walked =
|
|
6154
|
+
walked = join7(walked, segments[i]);
|
|
5275
6155
|
try {
|
|
5276
6156
|
if (lstatSync2(walked).isSymbolicLink())
|
|
5277
6157
|
return segments.slice(0, i + 1).join("/");
|
|
@@ -5289,12 +6169,12 @@ function symlinkEntry(path, link, isDirectoryRow) {
|
|
|
5289
6169
|
};
|
|
5290
6170
|
}
|
|
5291
6171
|
function applyFileRow(targetRepo, path, content) {
|
|
5292
|
-
const abs =
|
|
6172
|
+
const abs = join7(targetRepo, path);
|
|
5293
6173
|
const link = firstSymlinkOnPath(targetRepo, path);
|
|
5294
6174
|
if (link !== null)
|
|
5295
6175
|
return symlinkEntry(path, link, false);
|
|
5296
|
-
if (
|
|
5297
|
-
const current =
|
|
6176
|
+
if (existsSync5(abs)) {
|
|
6177
|
+
const current = readFileSync6(abs, "utf-8");
|
|
5298
6178
|
return current === content ? { path, outcome: "already-present", note: "already present and correct \u2014 nothing written" } : { path, outcome: "left-alone", note: "you already own this file \u2014 left exactly as it was" };
|
|
5299
6179
|
}
|
|
5300
6180
|
mkdirSync5(dirname4(abs), { recursive: true });
|
|
@@ -5302,17 +6182,17 @@ function applyFileRow(targetRepo, path, content) {
|
|
|
5302
6182
|
return { path, outcome: "created", note: "created" };
|
|
5303
6183
|
}
|
|
5304
6184
|
function applySectionRow(targetRepo, path, heading, body) {
|
|
5305
|
-
const abs =
|
|
6185
|
+
const abs = join7(targetRepo, path);
|
|
5306
6186
|
const link = firstSymlinkOnPath(targetRepo, path);
|
|
5307
6187
|
if (link !== null)
|
|
5308
6188
|
return symlinkEntry(path, link, false);
|
|
5309
6189
|
const section = renderSection(heading, body);
|
|
5310
|
-
if (!
|
|
6190
|
+
if (!existsSync5(abs)) {
|
|
5311
6191
|
mkdirSync5(dirname4(abs), { recursive: true });
|
|
5312
6192
|
writeFileSync5(abs, section, "utf-8");
|
|
5313
6193
|
return { path, outcome: "created", note: `created, with a "${heading}" section` };
|
|
5314
6194
|
}
|
|
5315
|
-
const existing =
|
|
6195
|
+
const existing = readFileSync6(abs, "utf-8");
|
|
5316
6196
|
if (hasSection(existing, heading)) {
|
|
5317
6197
|
return {
|
|
5318
6198
|
path,
|
|
@@ -5329,11 +6209,11 @@ function applySectionRow(targetRepo, path, heading, body) {
|
|
|
5329
6209
|
};
|
|
5330
6210
|
}
|
|
5331
6211
|
function applyDirectoryRow(targetRepo, path) {
|
|
5332
|
-
const abs =
|
|
6212
|
+
const abs = join7(targetRepo, path);
|
|
5333
6213
|
const link = firstSymlinkOnPath(targetRepo, path);
|
|
5334
6214
|
if (link !== null)
|
|
5335
6215
|
return symlinkEntry(path, link, true);
|
|
5336
|
-
if (
|
|
6216
|
+
if (existsSync5(abs)) {
|
|
5337
6217
|
const kind = statSync(abs).isDirectory() ? "directory" : "file";
|
|
5338
6218
|
return kind === "directory" ? { path: `${path}/`, outcome: "already-present", note: "already there \u2014 nothing written" } : { path, outcome: "left-alone", note: "you already own this path \u2014 left exactly as it was" };
|
|
5339
6219
|
}
|
|
@@ -5341,7 +6221,7 @@ function applyDirectoryRow(targetRepo, path) {
|
|
|
5341
6221
|
return { path: `${path}/`, outcome: "created", note: "created" };
|
|
5342
6222
|
}
|
|
5343
6223
|
function applySetupWrites(targetRepo, rows) {
|
|
5344
|
-
if (!
|
|
6224
|
+
if (!existsSync5(targetRepo)) {
|
|
5345
6225
|
throw new Error(`[halfcycle setup] The project directory does not exist: ${targetRepo}`);
|
|
5346
6226
|
}
|
|
5347
6227
|
assertNoConflictingDuplicates(rows);
|
|
@@ -5431,11 +6311,13 @@ var SetupManifest = class {
|
|
|
5431
6311
|
// dist/setup/rows.js
|
|
5432
6312
|
var FLAT_FILE_TRACKER_HOME = "docs/tasks";
|
|
5433
6313
|
function contextIndexRow(heading, body) {
|
|
5434
|
-
return { path: "
|
|
6314
|
+
return { path: "AGENTS.md", kind: "section", heading, body };
|
|
5435
6315
|
}
|
|
5436
|
-
function agentsPointerRow(
|
|
5437
|
-
return { path: "
|
|
6316
|
+
function agentsPointerRow() {
|
|
6317
|
+
return { path: "CLAUDE.md", kind: "section", heading: AGENTS_POINTER_HEADING, body: AGENTS_POINTER_BODY };
|
|
5438
6318
|
}
|
|
6319
|
+
var AGENTS_POINTER_HEADING = "Shared agent instructions";
|
|
6320
|
+
var AGENTS_POINTER_BODY = "@AGENTS.md";
|
|
5439
6321
|
function layerDocumentRow(path, content) {
|
|
5440
6322
|
return { path, kind: "file", content };
|
|
5441
6323
|
}
|
|
@@ -5450,8 +6332,8 @@ function trackerHomeRow(home = FLAT_FILE_TRACKER_HOME) {
|
|
|
5450
6332
|
}
|
|
5451
6333
|
|
|
5452
6334
|
// dist/setup/discovery.js
|
|
5453
|
-
import { existsSync as
|
|
5454
|
-
import { join as
|
|
6335
|
+
import { existsSync as existsSync6, readFileSync as readFileSync7, readdirSync, statSync as statSync2 } from "node:fs";
|
|
6336
|
+
import { join as join8 } from "node:path";
|
|
5455
6337
|
function renderAnchor(anchor) {
|
|
5456
6338
|
return anchor.kind === "file" ? `${anchor.path}:${anchor.line}` : `$ ${anchor.command}`;
|
|
5457
6339
|
}
|
|
@@ -5475,8 +6357,8 @@ function lineOf(text, needle) {
|
|
|
5475
6357
|
return null;
|
|
5476
6358
|
}
|
|
5477
6359
|
function readOrNull(repo, rel) {
|
|
5478
|
-
const abs =
|
|
5479
|
-
return
|
|
6360
|
+
const abs = join8(repo, rel);
|
|
6361
|
+
return existsSync6(abs) && statSync2(abs).isFile() ? readFileSync7(abs, "utf-8") : null;
|
|
5480
6362
|
}
|
|
5481
6363
|
function anchoredIn(repo, rel, needle, value) {
|
|
5482
6364
|
const text = readOrNull(repo, rel);
|
|
@@ -5488,8 +6370,8 @@ function anchoredIn(repo, rel, needle, value) {
|
|
|
5488
6370
|
return { value, anchor: { kind: "file", path: rel, line } };
|
|
5489
6371
|
}
|
|
5490
6372
|
function presenceAnchor(repo, rel) {
|
|
5491
|
-
const abs =
|
|
5492
|
-
if (!
|
|
6373
|
+
const abs = join8(repo, rel);
|
|
6374
|
+
if (!existsSync6(abs))
|
|
5493
6375
|
return null;
|
|
5494
6376
|
return statSync2(abs).isDirectory() ? { kind: "command", command: `ls -d ${rel}`, output: `${rel}/` } : { kind: "file", path: rel, line: 1 };
|
|
5495
6377
|
}
|
|
@@ -5528,9 +6410,10 @@ function firstMatch(items, f) {
|
|
|
5528
6410
|
}
|
|
5529
6411
|
return null;
|
|
5530
6412
|
}
|
|
6413
|
+
var CONTEXT_INDEX_FILES = ["AGENTS.md", "CLAUDE.md"];
|
|
5531
6414
|
function ciWorkflows(repo) {
|
|
5532
|
-
const dir =
|
|
5533
|
-
if (!
|
|
6415
|
+
const dir = join8(repo, ".github", "workflows");
|
|
6416
|
+
if (!existsSync6(dir) || !statSync2(dir).isDirectory())
|
|
5534
6417
|
return [];
|
|
5535
6418
|
return readdirSync(dir).filter((f) => f.endsWith(".yml") || f.endsWith(".yaml")).sort();
|
|
5536
6419
|
}
|
|
@@ -5538,21 +6421,21 @@ function existingLayerDocuments(repo) {
|
|
|
5538
6421
|
const found = [];
|
|
5539
6422
|
for (const paths of Object.values(LAYER_ARTEFACT_PATHS)) {
|
|
5540
6423
|
for (const p of paths ?? []) {
|
|
5541
|
-
if (
|
|
6424
|
+
if (existsSync6(join8(repo, p)))
|
|
5542
6425
|
found.push(p);
|
|
5543
6426
|
}
|
|
5544
6427
|
}
|
|
5545
6428
|
return found.sort();
|
|
5546
6429
|
}
|
|
5547
6430
|
function discoverRepository(targetRepo) {
|
|
5548
|
-
if (!
|
|
6431
|
+
if (!existsSync6(targetRepo)) {
|
|
5549
6432
|
throw new Error(`[halfcycle setup] The project directory does not exist: ${targetRepo}`);
|
|
5550
6433
|
}
|
|
5551
6434
|
const findings = [];
|
|
5552
6435
|
const questions = [];
|
|
5553
6436
|
settleOrAsk("project-language", firstMatch(MANIFESTS, ([file, needle, language]) => anchoredIn(targetRepo, file, needle, language)), "What is this project written in?", findings, questions);
|
|
5554
6437
|
settleOrAsk("package-manager", firstMatch(LOCKFILES, ([file, manager]) => anchoredByPresence(targetRepo, file, manager)), "Which package manager does this project use?", findings, questions);
|
|
5555
|
-
settleOrAsk("repo-structure", firstMatch(WORKSPACE_MARKERS, ([file, needle, shape]) => anchoredIn(targetRepo, file, needle, shape)) ?? (
|
|
6438
|
+
settleOrAsk("repo-structure", firstMatch(WORKSPACE_MARKERS, ([file, needle, shape]) => anchoredIn(targetRepo, file, needle, shape)) ?? (existsSync6(join8(targetRepo, "package.json")) || existsSync6(join8(targetRepo, "go.mod")) || existsSync6(join8(targetRepo, "pyproject.toml")) ? anchoredByPresence(targetRepo, "package.json", "a single-package repository") ?? anchoredByPresence(targetRepo, "go.mod", "a single-package repository") ?? anchoredByPresence(targetRepo, "pyproject.toml", "a single-package repository") : null), "Is this one repository for one product, or one of several?", findings, questions);
|
|
5556
6439
|
const workflows = ciWorkflows(targetRepo);
|
|
5557
6440
|
settleOrAsk("ci-strategy", workflows.length > 0 ? {
|
|
5558
6441
|
value: `GitHub Actions \u2014 ${workflows.length === 1 ? "one workflow" : `${workflows.length} workflows`}: ${workflows.join(", ")}`,
|
|
@@ -5560,7 +6443,7 @@ function discoverRepository(targetRepo) {
|
|
|
5560
6443
|
} : null, "How does this project run CI?", findings, questions);
|
|
5561
6444
|
const layerDocs = existingLayerDocuments(targetRepo);
|
|
5562
6445
|
settleOrAsk("existing-layer-documents", layerDocs.length > 0 ? anchoredByPresence(targetRepo, layerDocs[0], `already written: ${layerDocs.join(", ")}`) : null, "Which of the project documents already exist?", findings, questions);
|
|
5563
|
-
settleOrAsk("context-index", anchoredByPresence(targetRepo,
|
|
6446
|
+
settleOrAsk("context-index", firstMatch(CONTEXT_INDEX_FILES, (rel) => anchoredByPresence(targetRepo, rel, "a context index is already in place")), "Where should the project context index live?", findings, questions);
|
|
5564
6447
|
settleOrAsk("tracker", anchoredByPresence(targetRepo, "docs/tasks", "a flat-file task directory is already in place") ?? anchoredByPresence(targetRepo, ".github/ISSUE_TEMPLATE", "GitHub Issues is already in use"), "Where should tasks live?", findings, questions);
|
|
5565
6448
|
settleOrAsk("push-policy", null, "How does work reach the default branch?", findings, questions);
|
|
5566
6449
|
settleOrAsk("branch-policy", null, "How are branches organised for a piece of work?", findings, questions);
|
|
@@ -6023,10 +6906,14 @@ function renderClose(heading, observed, attested, checks, p, closingRound = fals
|
|
|
6023
6906
|
];
|
|
6024
6907
|
}
|
|
6025
6908
|
var REVIEW_LABEL = "**Recorded by Halfcycle from the owner's review.** Each part the review held, and how the project's owner decided it. A line marked as the agent's is the agent's own answer, not Halfcycle's.";
|
|
6909
|
+
var REVIEW_LABEL_WITH_AUTONOMY = `**Recorded by Halfcycle from the owner's review.** Each part the review held, and how it was decided: by the project's owner, unless the part says it was ${DECIDED_BY_AUTONOMY_SENTENCE}. A line marked as the agent's is the agent's own answer, not Halfcycle's.`;
|
|
6026
6910
|
var REVIEW_NO_PARTS = "The review had no parts, so it closed on one accept that names no part.";
|
|
6027
6911
|
function approvalLine(approval) {
|
|
6028
6912
|
if (approval.kind === "none")
|
|
6029
6913
|
return "Built without an approved scope.";
|
|
6914
|
+
if (approval.givenIn === "autonomy") {
|
|
6915
|
+
return `Plan version ${String(approval.planVersion)} was ${APPROVED_IN_ADVANCE_SENTENCE}.`;
|
|
6916
|
+
}
|
|
6030
6917
|
const where = approval.givenIn === "console" ? "in the console" : "in their own session";
|
|
6031
6918
|
return `Scope approved by the project's owner, plan version ${String(approval.planVersion)}, ${where}.`;
|
|
6032
6919
|
}
|
|
@@ -6040,8 +6927,11 @@ function decisionText(part, p) {
|
|
|
6040
6927
|
return `Dropped: ${p.text(part.reason ?? "")}`;
|
|
6041
6928
|
}
|
|
6042
6929
|
}
|
|
6930
|
+
var DECIDED_BY_AUTONOMY_LINE = `${DECIDED_BY_AUTONOMY_SENTENCE.charAt(0).toUpperCase()}${DECIDED_BY_AUTONOMY_SENTENCE.slice(1)}.`;
|
|
6043
6931
|
function reviewPartLines(part, p) {
|
|
6044
6932
|
const lines = [`- ${p.itemStart(part.name)} \u2014 ${decisionText(part, p)}`];
|
|
6933
|
+
if (part.givenIn === "autonomy")
|
|
6934
|
+
lines.push(` - ${DECIDED_BY_AUTONOMY_LINE}`);
|
|
6045
6935
|
const code = codeLinkLine(part.codeLink);
|
|
6046
6936
|
if (code !== void 0)
|
|
6047
6937
|
lines.push(` - ${p.text(code)}`);
|
|
@@ -6061,7 +6951,8 @@ function removalsLine(record2, p) {
|
|
|
6061
6951
|
}
|
|
6062
6952
|
function renderReview(record2, p) {
|
|
6063
6953
|
const review = record2.review;
|
|
6064
|
-
const
|
|
6954
|
+
const label = review.parts.some((part) => part.givenIn === "autonomy") ? REVIEW_LABEL_WITH_AUTONOMY : REVIEW_LABEL;
|
|
6955
|
+
const blocks = ["## The owner's review", label, approvalLine(review.approval)];
|
|
6065
6956
|
blocks.push(review.parts.length === 0 ? REVIEW_NO_PARTS : review.parts.flatMap((part) => reviewPartLines(part, p)).join("\n"));
|
|
6066
6957
|
const removals = removalsLine(record2, p);
|
|
6067
6958
|
if (removals !== null)
|
|
@@ -6073,10 +6964,9 @@ function renderCalls(calls, p) {
|
|
|
6073
6964
|
return [`## ${CALLS_HEADING}`, NO_CALLS_LINE];
|
|
6074
6965
|
const lines = calls.flatMap((c) => {
|
|
6075
6966
|
const by = p.itemStart(c.by);
|
|
6076
|
-
|
|
6077
|
-
|
|
6078
|
-
|
|
6079
|
-
];
|
|
6967
|
+
const undo = ` - ${callUndoLine({ by: p.text(c.by), undoable: c.undoable, how: c.how === null ? null : p.text(c.how) })}`;
|
|
6968
|
+
const head = c.kind === "listed" ? autonomyC8(by, p.text(c.question), p.text(c.chose)) : callMadeLine({ by, question: p.text(c.question), chose: p.text(c.chose), statedDefault: p.text(c.statedDefault) });
|
|
6969
|
+
return [`- ${head}`, undo];
|
|
6080
6970
|
});
|
|
6081
6971
|
return [`## ${CALLS_HEADING}`, CALLS_EXPLAINED, lines.join("\n")];
|
|
6082
6972
|
}
|
|
@@ -6176,11 +7066,11 @@ function renderBuildRecordV2Markdown(record2) {
|
|
|
6176
7066
|
}
|
|
6177
7067
|
|
|
6178
7068
|
// dist/build-record/write.js
|
|
6179
|
-
import { existsSync as
|
|
6180
|
-
import { basename, join as
|
|
7069
|
+
import { existsSync as existsSync7, mkdirSync as mkdirSync6, rmSync as rmSync4 } from "node:fs";
|
|
7070
|
+
import { basename, join as join9, resolve as resolve2, relative as relative2, isAbsolute as isAbsolute3 } from "node:path";
|
|
6181
7071
|
|
|
6182
7072
|
// dist/build-record/pair-io.js
|
|
6183
|
-
import { closeSync, openSync, readFileSync as
|
|
7073
|
+
import { closeSync, openSync, readFileSync as readFileSync8, renameSync as renameSync2, rmSync as rmSync3, writeSync } from "node:fs";
|
|
6184
7074
|
function createExclusiveOnDisk(path, data) {
|
|
6185
7075
|
const bytes = typeof data === "string" ? Buffer.from(data, "utf-8") : data;
|
|
6186
7076
|
const fd = openSync(path, "wx");
|
|
@@ -6211,7 +7101,7 @@ function isEexist(err) {
|
|
|
6211
7101
|
}
|
|
6212
7102
|
function readIfPresent(path) {
|
|
6213
7103
|
try {
|
|
6214
|
-
return
|
|
7104
|
+
return readFileSync8(path, "utf-8");
|
|
6215
7105
|
} catch {
|
|
6216
7106
|
return null;
|
|
6217
7107
|
}
|
|
@@ -6243,7 +7133,7 @@ function replaceOne(io, tmp, finalPath, text) {
|
|
|
6243
7133
|
}
|
|
6244
7134
|
|
|
6245
7135
|
// dist/build-record/write.js
|
|
6246
|
-
var BUILD_RECORD_DIR =
|
|
7136
|
+
var BUILD_RECORD_DIR = join9("docs", "build-records");
|
|
6247
7137
|
function assertInsideOutDir(outDir, candidate, identity) {
|
|
6248
7138
|
const rel = relative2(outDir, resolve2(candidate));
|
|
6249
7139
|
const isPlainFilename = rel !== "" && rel !== ".." && !isAbsolute3(rel) && rel === basename(rel);
|
|
@@ -6281,8 +7171,8 @@ function writeRecordPair(repoRoot, phase, rawRecord, record2, io = NODE_RECORD_P
|
|
|
6281
7171
|
}
|
|
6282
7172
|
const outDir = resolve2(repoRoot, BUILD_RECORD_DIR);
|
|
6283
7173
|
const segment = renderPhaseSegment(phase);
|
|
6284
|
-
const jsonPath =
|
|
6285
|
-
const mdPath =
|
|
7174
|
+
const jsonPath = join9(outDir, `${segment}.json`);
|
|
7175
|
+
const mdPath = join9(outDir, `${segment}.md`);
|
|
6286
7176
|
const jsonTmp = `${jsonPath}.tmp`;
|
|
6287
7177
|
const mdTmp = `${mdPath}.tmp`;
|
|
6288
7178
|
for (const candidate of [jsonPath, mdPath, jsonTmp, mdTmp]) {
|
|
@@ -6296,7 +7186,7 @@ function writeRecordPair(repoRoot, phase, rawRecord, record2, io = NODE_RECORD_P
|
|
|
6296
7186
|
}
|
|
6297
7187
|
const existingJson = readIfPresent(jsonPath);
|
|
6298
7188
|
if (existingJson === null) {
|
|
6299
|
-
return { kind: "in-the-way", path:
|
|
7189
|
+
return { kind: "in-the-way", path: existsSync7(jsonPath) ? jsonPath : mdPath };
|
|
6300
7190
|
}
|
|
6301
7191
|
if (existingJson === jsonText) {
|
|
6302
7192
|
replaceOne(io, mdTmp, mdPath, mdText);
|