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