dld-kit 1.0.0-rc.2 → 1.0.0-rc.4
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/README.md +109 -333
- package/dist/dld.mjs +327 -90
- package/package.json +1 -1
- package/skills/dld-adjust/SKILL.md +1 -1
- package/skills/dld-audit/SKILL.md +1 -1
- package/skills/dld-audit-auto/SKILL.md +1 -1
- package/skills/dld-common/SKILL.md +1 -1
- package/skills/dld-common/scripts/dld.mjs +327 -90
- package/skills/dld-decide/SKILL.md +1 -1
- package/skills/dld-implement/SKILL.md +1 -1
- package/skills/dld-init/SKILL.md +1 -1
- package/skills/dld-lookup/SKILL.md +1 -1
- package/skills/dld-plan/SKILL.md +1 -1
- package/skills/dld-reindex/SKILL.md +70 -23
- package/skills/dld-retrofit/SKILL.md +1 -1
- package/skills/dld-snapshot/SKILL.md +1 -1
- package/skills/dld-status/SKILL.md +1 -1
- package/templates/rules/dld-workflow.md +1 -1
- package/templates/skills/dld-reindex/SKILL.md +69 -22
|
@@ -7372,7 +7372,7 @@ import { createInterface } from "node:readline/promises";
|
|
|
7372
7372
|
import { fileURLToPath } from "node:url";
|
|
7373
7373
|
|
|
7374
7374
|
// package.json
|
|
7375
|
-
var version = "1.0.0-rc.
|
|
7375
|
+
var version = "1.0.0-rc.4";
|
|
7376
7376
|
|
|
7377
7377
|
// src/core/errors.ts
|
|
7378
7378
|
var EXIT_ERROR = 1;
|
|
@@ -7902,11 +7902,11 @@ function detectSnapshotChanges(ctx, project) {
|
|
|
7902
7902
|
if ((stored === void 0 || stored === "unknown") && lastRun !== void 0) {
|
|
7903
7903
|
stored = gitOrEmpty(git, "log", `--until=${lastRun}`, "--format=%h", "-1").trim() || void 0;
|
|
7904
7904
|
}
|
|
7905
|
-
const
|
|
7905
|
+
const commit2 = resolveStateCommit(git, stored);
|
|
7906
7906
|
let modifiedDecisions = [];
|
|
7907
7907
|
let commitRange = "";
|
|
7908
|
-
if (
|
|
7909
|
-
commitRange = `${
|
|
7908
|
+
if (commit2 !== void 0 && commit2 !== shortHead(ctx, paths.root)) {
|
|
7909
|
+
commitRange = `${commit2}..HEAD`;
|
|
7910
7910
|
const numbers = nulSeparated(
|
|
7911
7911
|
gitOrEmpty(git, "diff", "-z", "--name-only", commitRange, "--", recordsPathspec(paths))
|
|
7912
7912
|
).map((path) => basename3(path)).filter((name) => RECORD_FILE.test(name)).map((name) => recordNumber(name)).filter((number) => number <= includedNumber);
|
|
@@ -7927,23 +7927,24 @@ function formatSnapshotChanges(changes) {
|
|
|
7927
7927
|
function updateSnapshotState(ctx, project, customArtifacts) {
|
|
7928
7928
|
const { paths } = project;
|
|
7929
7929
|
const timestamp = formatTimestamp(ctx.now());
|
|
7930
|
-
const
|
|
7930
|
+
const commit2 = shortHead(ctx, paths.root);
|
|
7931
7931
|
const highest = readRecords(ctx, project).filter((record) => record.accepted).reduce((max, record) => Math.max(max, record.number), 0);
|
|
7932
7932
|
const artifacts = /* @__PURE__ */ Object.create(null);
|
|
7933
7933
|
for (const name of [...BUILT_IN_ARTIFACTS, ...customArtifacts]) artifacts[name] = timestamp;
|
|
7934
7934
|
writeStateSection(ctx, paths, "snapshot", {
|
|
7935
7935
|
last_run: timestamp,
|
|
7936
|
-
commit_hash:
|
|
7936
|
+
commit_hash: commit2,
|
|
7937
7937
|
decisions_included: String(highest),
|
|
7938
7938
|
artifacts
|
|
7939
7939
|
});
|
|
7940
|
-
return { timestamp, commit, highest };
|
|
7940
|
+
return { timestamp, commit: commit2, highest };
|
|
7941
7941
|
}
|
|
7942
7942
|
|
|
7943
7943
|
// src/cli/commands/collect-active-decisions.ts
|
|
7944
7944
|
var collectActiveDecisionsCommand = {
|
|
7945
7945
|
name: "collect-active-decisions",
|
|
7946
7946
|
summary: "Print every accepted decision record",
|
|
7947
|
+
internal: true,
|
|
7947
7948
|
usage: "Usage: dld collect-active-decisions\n\nPrint each accepted record in ID order, separated by ===DLD_DECISION_BOUNDARY=== lines.\n",
|
|
7948
7949
|
run(args, io, ctx) {
|
|
7949
7950
|
parseCommandArgs({ args: [...args], options: {} });
|
|
@@ -7961,8 +7962,9 @@ import { basename as basename4 } from "node:path";
|
|
|
7961
7962
|
// src/core/open-prs.ts
|
|
7962
7963
|
var GITHUB_REMOTE = /github\.com[:/]/;
|
|
7963
7964
|
var PR_LIMIT = "100";
|
|
7965
|
+
var COMMIT_ID = /^[0-9a-f]{40}([0-9a-f]{24})?$/;
|
|
7964
7966
|
function openPrIds(ctx, paths, base) {
|
|
7965
|
-
const skipped = (reason) => ({
|
|
7967
|
+
const skipped = (reason) => ({ claims: [], skipped: reason });
|
|
7966
7968
|
try {
|
|
7967
7969
|
ctx.gh(["--version"]);
|
|
7968
7970
|
} catch (error) {
|
|
@@ -7989,7 +7991,7 @@ function openPrIds(ctx, paths, base) {
|
|
|
7989
7991
|
"open",
|
|
7990
7992
|
`--base=${prBase}`,
|
|
7991
7993
|
"--json",
|
|
7992
|
-
"files,headRefName,isCrossRepository",
|
|
7994
|
+
"files,headRefName,headRefOid,isCrossRepository",
|
|
7993
7995
|
"--limit",
|
|
7994
7996
|
PR_LIMIT
|
|
7995
7997
|
]);
|
|
@@ -8003,14 +8005,15 @@ function openPrIds(ctx, paths, base) {
|
|
|
8003
8005
|
if (prs === void 0) return skipped("gh pr list returned unexpected output");
|
|
8004
8006
|
const current = gitOrEmpty(git, "rev-parse", "--abbrev-ref", "HEAD").trim();
|
|
8005
8007
|
const prefix = `${recordsPathspec(paths)}/`;
|
|
8006
|
-
const
|
|
8008
|
+
const claims = [];
|
|
8007
8009
|
for (const pr of prs) {
|
|
8008
8010
|
if (!pr.isCrossRepository && pr.headRefName === current) continue;
|
|
8009
8011
|
for (const path of pr.files) {
|
|
8010
|
-
if (path.startsWith(prefix))
|
|
8012
|
+
if (!path.startsWith(prefix)) continue;
|
|
8013
|
+
for (const id of path.match(DECISION_MENTION) ?? []) claims.push({ id, head: pr.headRefOid });
|
|
8011
8014
|
}
|
|
8012
8015
|
}
|
|
8013
|
-
return {
|
|
8016
|
+
return { claims };
|
|
8014
8017
|
}
|
|
8015
8018
|
function parsePrList(output) {
|
|
8016
8019
|
let value;
|
|
@@ -8026,6 +8029,7 @@ function parsePrList(output) {
|
|
|
8026
8029
|
const files = Array.isArray(item.files) ? item.files : [];
|
|
8027
8030
|
prs.push({
|
|
8028
8031
|
headRefName: typeof item.headRefName === "string" ? item.headRefName : void 0,
|
|
8032
|
+
headRefOid: typeof item.headRefOid === "string" && COMMIT_ID.test(item.headRefOid) ? item.headRefOid : void 0,
|
|
8029
8033
|
isCrossRepository: item.isCrossRepository === true,
|
|
8030
8034
|
files: files.flatMap(
|
|
8031
8035
|
(file) => isObject(file) && typeof file.path === "string" ? [file.path] : []
|
|
@@ -8094,24 +8098,56 @@ function takenIds(ctx, paths, base) {
|
|
|
8094
8098
|
gitOrEmpty(git, "ls-tree", "-r", "-z", "--name-only", base, "--", recordsPathspec(paths))
|
|
8095
8099
|
).flatMap((path) => path.match(DECISION_MENTION) ?? []);
|
|
8096
8100
|
const scan = openPrIds(ctx, paths, base);
|
|
8097
|
-
const ids = [.../* @__PURE__ */ new Set([...onBase, ...scan.
|
|
8098
|
-
|
|
8101
|
+
const ids = [.../* @__PURE__ */ new Set([...onBase, ...scan.claims.map(({ id }) => id)])].sort(compareIds);
|
|
8102
|
+
const taken = { ids, onBase: new Set(onBase), claims: scan.claims };
|
|
8103
|
+
return scan.skipped === void 0 ? taken : { ...taken, skipped: scan.skipped };
|
|
8104
|
+
}
|
|
8105
|
+
function isAncestor(git, ancestor, commit2) {
|
|
8106
|
+
try {
|
|
8107
|
+
git("merge-base", "--is-ancestor", ancestor, commit2);
|
|
8108
|
+
return true;
|
|
8109
|
+
} catch (error) {
|
|
8110
|
+
if (error instanceof GitCommandError) return false;
|
|
8111
|
+
throw error;
|
|
8112
|
+
}
|
|
8113
|
+
}
|
|
8114
|
+
function collides(git, base, { path, id }, taken) {
|
|
8115
|
+
if (taken.onBase.has(id)) return true;
|
|
8116
|
+
const claims = taken.claims.filter((claim) => claim.id === id);
|
|
8117
|
+
if (claims.length === 0) return false;
|
|
8118
|
+
const added = git(
|
|
8119
|
+
"--literal-pathspecs",
|
|
8120
|
+
"log",
|
|
8121
|
+
// log.follow or log.showSignature in the user's config would change the output.
|
|
8122
|
+
"--no-follow",
|
|
8123
|
+
"--no-show-signature",
|
|
8124
|
+
"-1",
|
|
8125
|
+
"--format=%H",
|
|
8126
|
+
"--diff-filter=A",
|
|
8127
|
+
`${base}..HEAD`,
|
|
8128
|
+
"--",
|
|
8129
|
+
path
|
|
8130
|
+
).trim();
|
|
8131
|
+
return claims.some(
|
|
8132
|
+
({ head }) => head === void 0 || added === "" || !isAncestor(git, added, head)
|
|
8133
|
+
);
|
|
8099
8134
|
}
|
|
8100
8135
|
function listTakenIds(ctx, { paths }, base) {
|
|
8101
8136
|
verifyBase(ctx, paths, base, " Fetch first or pass --base.");
|
|
8102
|
-
|
|
8137
|
+
const { ids, skipped } = takenIds(ctx, paths, base);
|
|
8138
|
+
return skipped === void 0 ? { ids } : { ids, skipped };
|
|
8103
8139
|
}
|
|
8104
8140
|
function collisionsOf(ctx, paths, base) {
|
|
8105
|
-
const
|
|
8141
|
+
const git = gitAt(ctx, paths.root);
|
|
8142
|
+
const added = localAdditions(git, paths, base);
|
|
8106
8143
|
const local = added.flatMap((path) => {
|
|
8107
8144
|
const id = basename4(path, ".md");
|
|
8108
8145
|
return RECORD_FILE.test(basename4(path)) ? [{ path, id }] : [];
|
|
8109
8146
|
});
|
|
8110
8147
|
if (local.length === 0) return { collisions: [], localIds: [], taken: [] };
|
|
8111
8148
|
const taken = takenIds(ctx, paths, base);
|
|
8112
|
-
const takenSet = new Set(taken.ids);
|
|
8113
8149
|
const result = {
|
|
8114
|
-
collisions: local.filter((
|
|
8150
|
+
collisions: local.filter((record) => collides(git, base, record, taken)),
|
|
8115
8151
|
localIds: local.map(({ id }) => id),
|
|
8116
8152
|
taken: taken.ids
|
|
8117
8153
|
};
|
|
@@ -8189,7 +8225,7 @@ function parseRenamePlan(paths, text) {
|
|
|
8189
8225
|
|
|
8190
8226
|
// src/core/commit-reindex.ts
|
|
8191
8227
|
var SUBJECT_LIST_LIMIT = 3;
|
|
8192
|
-
function reindexMessage(renames, originalSubjects) {
|
|
8228
|
+
function reindexMessage(renames, originalSubjects = "") {
|
|
8193
8229
|
const pairs = renames.map(({ oldId, newId }) => `${oldId} -> ${newId}`);
|
|
8194
8230
|
const subject = renames.length > SUBJECT_LIST_LIMIT ? `reindex ${renames.length} local decisions to avoid base-branch collisions` : `reindex local decisions: ${pairs.join(", ")}`;
|
|
8195
8231
|
const message = `${subject}
|
|
@@ -8201,11 +8237,75 @@ ${pairs.map((pair) => `- ${pair}`).join("\n")}`;
|
|
|
8201
8237
|
Squashed from original branch commits:
|
|
8202
8238
|
${originalSubjects}`;
|
|
8203
8239
|
}
|
|
8204
|
-
function commitReindex(ctx, { paths }, planText, base) {
|
|
8205
|
-
const git = gitAt(ctx, paths.root);
|
|
8240
|
+
function commitReindex(ctx, { paths }, planText, base, { squash = false } = {}) {
|
|
8206
8241
|
verifyBase(ctx, paths, base);
|
|
8207
8242
|
const renames = parseRenamePlan(paths, planText);
|
|
8208
8243
|
if (renames.length === 0) throw new DldError("no rename plan on stdin.");
|
|
8244
|
+
for (const { oldId, newId, path } of renames) {
|
|
8245
|
+
const renamed = posix2.join(posix2.dirname(path), `${newId}.md`);
|
|
8246
|
+
if (!ctx.fs.isRegularFile(join7(paths.root, renamed))) {
|
|
8247
|
+
throw new DldError(`${renamed} not found. Run rename-decision for ${oldId} first.`);
|
|
8248
|
+
}
|
|
8249
|
+
}
|
|
8250
|
+
const git = gitAt(ctx, paths.root);
|
|
8251
|
+
return squash ? squashReindex(ctx, git, paths, renames, base) : commitOnTop(ctx, git, paths, renames);
|
|
8252
|
+
}
|
|
8253
|
+
function commitOnTop(ctx, git, paths, renames) {
|
|
8254
|
+
const stage = /* @__PURE__ */ new Set();
|
|
8255
|
+
for (const { path, newId } of renames) {
|
|
8256
|
+
stage.add(path);
|
|
8257
|
+
stage.add(posix2.join(posix2.dirname(path), `${newId}.md`));
|
|
8258
|
+
}
|
|
8259
|
+
for (const file of nulSeparated(git("diff", "-z", "--no-renames", "--name-only", "HEAD"))) {
|
|
8260
|
+
stage.add(file);
|
|
8261
|
+
}
|
|
8262
|
+
const head = git("rev-parse", "HEAD").trim();
|
|
8263
|
+
const tree = git("write-tree").trim();
|
|
8264
|
+
let stepName = "staging";
|
|
8265
|
+
try {
|
|
8266
|
+
for (const path of [...stage].sort()) {
|
|
8267
|
+
stepName = `staging ${path}`;
|
|
8268
|
+
stagePath(ctx, git, join7(paths.root, path), path);
|
|
8269
|
+
}
|
|
8270
|
+
stepName = "checking the staged changes";
|
|
8271
|
+
if (!hasStagedChanges(git)) {
|
|
8272
|
+
throw new DldError(
|
|
8273
|
+
"nothing to commit. The reindex may have already been committed, or the plan didn't match the working tree."
|
|
8274
|
+
);
|
|
8275
|
+
}
|
|
8276
|
+
stepName = "committing";
|
|
8277
|
+
commit(git, reindexMessage(renames));
|
|
8278
|
+
} catch (error) {
|
|
8279
|
+
try {
|
|
8280
|
+
git("read-tree", tree);
|
|
8281
|
+
} catch (restoreError) {
|
|
8282
|
+
throw new DldError(
|
|
8283
|
+
`commit-reindex failed while ${stepName}: ${describe(error)}
|
|
8284
|
+
Restoring the index also failed: ${describe(restoreError)}
|
|
8285
|
+
To restore by hand: git read-tree ${tree}`
|
|
8286
|
+
);
|
|
8287
|
+
}
|
|
8288
|
+
if (!(error instanceof DldError)) throw error;
|
|
8289
|
+
throw new DldError(
|
|
8290
|
+
`commit-reindex failed while ${stepName}: ${error.message}
|
|
8291
|
+
The index was restored.`,
|
|
8292
|
+
error.exitCode
|
|
8293
|
+
);
|
|
8294
|
+
}
|
|
8295
|
+
return {
|
|
8296
|
+
commit: git("rev-parse", "--short", "HEAD").trim(),
|
|
8297
|
+
onto: git("rev-parse", "--short", head).trim()
|
|
8298
|
+
};
|
|
8299
|
+
}
|
|
8300
|
+
function commit(git, message) {
|
|
8301
|
+
try {
|
|
8302
|
+
git("commit", "--quiet", "-m", message);
|
|
8303
|
+
} catch (error) {
|
|
8304
|
+
if (error instanceof GitCommandError) throw new DldError(`git commit failed: ${error.stderr}`);
|
|
8305
|
+
throw error;
|
|
8306
|
+
}
|
|
8307
|
+
}
|
|
8308
|
+
function squashReindex(ctx, git, paths, renames, base) {
|
|
8209
8309
|
const onto = mergeBase(ctx, paths, base);
|
|
8210
8310
|
const head = git("rev-parse", "HEAD").trim();
|
|
8211
8311
|
if (onto === head) throw new DldError("HEAD is already at the merge-base \u2014 nothing to squash.");
|
|
@@ -8254,15 +8354,7 @@ function commitReindex(ctx, { paths }, planText, base) {
|
|
|
8254
8354
|
);
|
|
8255
8355
|
}
|
|
8256
8356
|
});
|
|
8257
|
-
step("committing", () =>
|
|
8258
|
-
try {
|
|
8259
|
-
git("commit", "--quiet", "-m", message);
|
|
8260
|
-
} catch (error) {
|
|
8261
|
-
if (error instanceof GitCommandError)
|
|
8262
|
-
throw new DldError(`git commit failed: ${error.stderr}`);
|
|
8263
|
-
throw error;
|
|
8264
|
-
}
|
|
8265
|
-
});
|
|
8357
|
+
step("committing", () => commit(git, message));
|
|
8266
8358
|
} catch (error) {
|
|
8267
8359
|
throw rollback(ctx, git, indexFull, saved, stepName, error);
|
|
8268
8360
|
}
|
|
@@ -8325,23 +8417,35 @@ function skippedNotice(reason) {
|
|
|
8325
8417
|
// src/cli/commands/commit-reindex.ts
|
|
8326
8418
|
var commitReindexCommand = {
|
|
8327
8419
|
name: "commit-reindex",
|
|
8328
|
-
summary: "
|
|
8329
|
-
|
|
8420
|
+
summary: "Commit the renamed decisions",
|
|
8421
|
+
internal: true,
|
|
8422
|
+
usage: `Usage: dld commit-reindex --base <ref> [--squash] < plan
|
|
8423
|
+
|
|
8424
|
+
Read a rename plan on standard input and commit the renames, which rename-decision has
|
|
8425
|
+
already applied. By default this adds one commit on top of HEAD with the plan's paths and
|
|
8426
|
+
every tracked file changed since HEAD; untracked files are left out. On failure the index is
|
|
8427
|
+
restored.
|
|
8330
8428
|
|
|
8331
|
-
|
|
8332
|
-
|
|
8333
|
-
|
|
8429
|
+
With --squash, the branch's commits since the merge-base with <ref> are squashed into one
|
|
8430
|
+
reindex commit instead, and INDEX.md is left at its merge-base state. On failure the branch,
|
|
8431
|
+
index and INDEX.md are restored.
|
|
8334
8432
|
|
|
8335
8433
|
Options:
|
|
8336
8434
|
--base <ref> Base ref (required)
|
|
8435
|
+
--squash Squash the branch instead of adding a commit
|
|
8337
8436
|
`,
|
|
8338
8437
|
run(args, io, ctx) {
|
|
8339
|
-
const { values } = parseCommandArgs({
|
|
8438
|
+
const { values } = parseCommandArgs({
|
|
8439
|
+
args: [...args],
|
|
8440
|
+
options: { base: { type: "string" }, squash: { type: "boolean" } }
|
|
8441
|
+
});
|
|
8340
8442
|
if (!values.base) throw new DldError("--base is required.");
|
|
8341
8443
|
const base = baseOption(values.base);
|
|
8342
8444
|
const project = loadProject(ctx);
|
|
8343
|
-
const { commit, onto } = commitReindex(ctx, project, ctx.readStdin(), base
|
|
8344
|
-
|
|
8445
|
+
const { commit: commit2, onto } = commitReindex(ctx, project, ctx.readStdin(), base, {
|
|
8446
|
+
squash: values.squash === true
|
|
8447
|
+
});
|
|
8448
|
+
io.stdout(`Created reindex commit ${commit2} on top of ${onto}
|
|
8345
8449
|
`);
|
|
8346
8450
|
return EXIT_OK;
|
|
8347
8451
|
}
|
|
@@ -8386,6 +8490,7 @@ function createDirectories(ctx, { config, paths }) {
|
|
|
8386
8490
|
var createConfigCommand = {
|
|
8387
8491
|
name: "create-config",
|
|
8388
8492
|
summary: "Create dld.config.yaml at the project root",
|
|
8493
|
+
internal: true,
|
|
8389
8494
|
usage: "Usage: dld create-config <flat|namespaced> [namespace ...]\n\nCreate dld.config.yaml. Namespaced mode needs at least one namespace.\n",
|
|
8390
8495
|
run(args, io, ctx) {
|
|
8391
8496
|
const { positionals } = parseCommandArgs({
|
|
@@ -8443,6 +8548,7 @@ function isSafeDirName(name) {
|
|
|
8443
8548
|
var createDecisionCommand = {
|
|
8444
8549
|
name: "create-decision",
|
|
8445
8550
|
summary: "Create a proposed decision record",
|
|
8551
|
+
internal: true,
|
|
8446
8552
|
usage: `Usage: dld create-decision --id <DL-NNN> --title <title> [options]
|
|
8447
8553
|
|
|
8448
8554
|
Create a decision record with status proposed and print its path.
|
|
@@ -8494,6 +8600,7 @@ Options:
|
|
|
8494
8600
|
var createDirectoriesCommand = {
|
|
8495
8601
|
name: "create-directories",
|
|
8496
8602
|
summary: "Create the decisions directory structure from the config",
|
|
8603
|
+
internal: true,
|
|
8497
8604
|
usage: "Usage: dld create-directories\n\nCreate the decisions and records directories, and one per namespace.\n",
|
|
8498
8605
|
run(args, io, ctx) {
|
|
8499
8606
|
parseCommandArgs({ args: [...args], options: {} });
|
|
@@ -8562,6 +8669,7 @@ function writeIndex(ctx, paths, content) {
|
|
|
8562
8669
|
var createEmptyIndexCommand = {
|
|
8563
8670
|
name: "create-empty-index",
|
|
8564
8671
|
summary: "Write an INDEX.md with no decisions",
|
|
8672
|
+
internal: true,
|
|
8565
8673
|
usage: "Usage: dld create-empty-index\n\nWrite INDEX.md in the decisions directory with only the table header.\n",
|
|
8566
8674
|
run(args, io, ctx) {
|
|
8567
8675
|
parseCommandArgs({ args: [...args], options: {} });
|
|
@@ -8577,6 +8685,7 @@ var createEmptyIndexCommand = {
|
|
|
8577
8685
|
var detectSnapshotChangesCommand = {
|
|
8578
8686
|
name: "detect-snapshot-changes",
|
|
8579
8687
|
summary: "Report what changed since the last snapshot",
|
|
8688
|
+
internal: true,
|
|
8580
8689
|
usage: "Usage: dld detect-snapshot-changes\n\nPrint mode (full or incremental), new_decisions, modified_decisions and commit_range.\n",
|
|
8581
8690
|
run(args, io, ctx) {
|
|
8582
8691
|
parseCommandArgs({ args: [...args], options: {} });
|
|
@@ -8589,7 +8698,14 @@ var detectSnapshotChangesCommand = {
|
|
|
8589
8698
|
import { join as join11, relative as relative6, sep as sep3 } from "node:path";
|
|
8590
8699
|
var EXCLUDED_DIRS = /* @__PURE__ */ new Set([
|
|
8591
8700
|
".git",
|
|
8701
|
+
// Agent configuration directories, which hold installed skills.
|
|
8592
8702
|
".claude",
|
|
8703
|
+
".agents",
|
|
8704
|
+
".agent",
|
|
8705
|
+
".codex",
|
|
8706
|
+
".cursor",
|
|
8707
|
+
".opencode",
|
|
8708
|
+
".pi",
|
|
8593
8709
|
".tessl",
|
|
8594
8710
|
"node_modules",
|
|
8595
8711
|
"vendor",
|
|
@@ -8668,6 +8784,7 @@ function formatAnnotation({ file, line, id }) {
|
|
|
8668
8784
|
var findAnnotationsCommand = {
|
|
8669
8785
|
name: "find-annotations",
|
|
8670
8786
|
summary: "List every annotation in the codebase",
|
|
8787
|
+
internal: true,
|
|
8671
8788
|
usage: "Usage: dld find-annotations\n\nPrint <file>:<line>:<DL-NNN> for every annotation, one per line.\n",
|
|
8672
8789
|
run(args, io, ctx) {
|
|
8673
8790
|
parseCommandArgs({ args: [...args], options: {} });
|
|
@@ -8683,6 +8800,7 @@ var findAnnotationsCommand = {
|
|
|
8683
8800
|
var findCollisionsCommand = {
|
|
8684
8801
|
name: "find-collisions",
|
|
8685
8802
|
summary: "List locally added decisions whose IDs are taken",
|
|
8803
|
+
internal: true,
|
|
8686
8804
|
usage: `Usage: dld find-collisions [--base <ref>]
|
|
8687
8805
|
|
|
8688
8806
|
Print <path>\\t<DL-NNN> for each decision added on this branch whose ID is taken on the base
|
|
@@ -8710,11 +8828,11 @@ var idNumber2 = (id) => Number.parseInt(id.slice(3), 10);
|
|
|
8710
8828
|
function recordsChangedSinceAudit(ctx, { paths }) {
|
|
8711
8829
|
const git = gitAt(ctx, paths.root);
|
|
8712
8830
|
const stored = stateString(readStateSection(ctx, paths, "audit"), "commit_hash");
|
|
8713
|
-
const
|
|
8714
|
-
if (
|
|
8831
|
+
const commit2 = resolveStateCommit(git, stored);
|
|
8832
|
+
if (commit2 === void 0) return void 0;
|
|
8715
8833
|
const records = recordsPathspec(paths);
|
|
8716
8834
|
return /* @__PURE__ */ new Set([
|
|
8717
|
-
...nulSeparated(git("diff", "-z", "--name-only",
|
|
8835
|
+
...nulSeparated(git("diff", "-z", "--name-only", commit2, "--", records)),
|
|
8718
8836
|
...nulSeparated(git("ls-files", "-z", "--others", "--exclude-standard", "--", records))
|
|
8719
8837
|
]);
|
|
8720
8838
|
}
|
|
@@ -8740,15 +8858,16 @@ function findMissingAmends(ctx, project, { all }) {
|
|
|
8740
8858
|
}
|
|
8741
8859
|
function updateAuditState(ctx, { paths }) {
|
|
8742
8860
|
const timestamp = formatTimestamp(ctx.now());
|
|
8743
|
-
const
|
|
8744
|
-
writeStateSection(ctx, paths, "audit", { last_run: timestamp, commit_hash:
|
|
8745
|
-
return { timestamp, commit };
|
|
8861
|
+
const commit2 = shortHead(ctx, paths.root);
|
|
8862
|
+
writeStateSection(ctx, paths, "audit", { last_run: timestamp, commit_hash: commit2 });
|
|
8863
|
+
return { timestamp, commit: commit2 };
|
|
8746
8864
|
}
|
|
8747
8865
|
|
|
8748
8866
|
// src/cli/commands/find-missing-amends.ts
|
|
8749
8867
|
var findMissingAmendsCommand = {
|
|
8750
8868
|
name: "find-missing-amends",
|
|
8751
8869
|
summary: "List decision IDs mentioned in a body but not declared",
|
|
8870
|
+
internal: true,
|
|
8752
8871
|
usage: `Usage: dld find-missing-amends [--all]
|
|
8753
8872
|
|
|
8754
8873
|
Print <source-id>:<referenced-id> for each decision ID a record's body mentions without
|
|
@@ -8770,7 +8889,7 @@ Options:
|
|
|
8770
8889
|
import { Buffer as Buffer2 } from "node:buffer";
|
|
8771
8890
|
import { join as join12, posix as posix3 } from "node:path";
|
|
8772
8891
|
var idPattern = (id, flags = "") => new RegExp(`${id}(?![0-9])`, flags);
|
|
8773
|
-
function changedFiles(ctx, paths,
|
|
8892
|
+
function changedFiles(ctx, paths, commit2) {
|
|
8774
8893
|
return nulSeparated(
|
|
8775
8894
|
gitAt(ctx, paths.root)(
|
|
8776
8895
|
"diff",
|
|
@@ -8778,7 +8897,7 @@ function changedFiles(ctx, paths, commit) {
|
|
|
8778
8897
|
"--find-renames",
|
|
8779
8898
|
"--name-only",
|
|
8780
8899
|
"--diff-filter=AMR",
|
|
8781
|
-
|
|
8900
|
+
commit2
|
|
8782
8901
|
)
|
|
8783
8902
|
);
|
|
8784
8903
|
}
|
|
@@ -8829,7 +8948,7 @@ function renameDecision(ctx, project, rename, base) {
|
|
|
8829
8948
|
}
|
|
8830
8949
|
function findStaleMentions(ctx, { paths }, planText, base) {
|
|
8831
8950
|
const renames = parseRenamePlan(paths, planText);
|
|
8832
|
-
if (renames.length === 0)
|
|
8951
|
+
if (renames.length === 0) throw new DldError("no rename plan on stdin.");
|
|
8833
8952
|
verifyBase(ctx, paths, base);
|
|
8834
8953
|
const decisions = `${decisionsPathspec(paths)}/`;
|
|
8835
8954
|
const files = changedFiles(ctx, paths, mergeBase(ctx, paths, base)).flatMap((file) => {
|
|
@@ -8859,6 +8978,7 @@ function formatStaleMention({ file, line, oldId, newId, text }) {
|
|
|
8859
8978
|
var findStaleMentionsCommand = {
|
|
8860
8979
|
name: "find-stale-mentions",
|
|
8861
8980
|
summary: "List remaining mentions of renamed decision IDs",
|
|
8981
|
+
internal: true,
|
|
8862
8982
|
usage: `Usage: dld find-stale-mentions --base <ref> < plan
|
|
8863
8983
|
|
|
8864
8984
|
Read a rename plan (<path>\\t<DL-OLD>\\t<DL-NEW> per line) on standard input and print
|
|
@@ -9040,7 +9160,6 @@ var AGENTS_LAYOUT = {
|
|
|
9040
9160
|
dir: ".agents/skills"
|
|
9041
9161
|
};
|
|
9042
9162
|
var LAYOUTS = [CLAUDE_LAYOUT, AGENTS_LAYOUT];
|
|
9043
|
-
var AGENTS_FIRST = ["AGENTS.md", "CLAUDE.md"];
|
|
9044
9163
|
var HARNESSES = [
|
|
9045
9164
|
{
|
|
9046
9165
|
name: "claude",
|
|
@@ -9048,7 +9167,7 @@ var HARNESSES = [
|
|
|
9048
9167
|
layout: CLAUDE_LAYOUT,
|
|
9049
9168
|
rule: "claude-file",
|
|
9050
9169
|
markers: [".claude", "CLAUDE.md"],
|
|
9051
|
-
// @decision(DL-054)
|
|
9170
|
+
// @decision(DL-054) @decision(DL-055)
|
|
9052
9171
|
instructions: ["CLAUDE.md", ".claude/CLAUDE.md", "CLAUDE.local.md", "AGENTS.md"]
|
|
9053
9172
|
},
|
|
9054
9173
|
{
|
|
@@ -9073,7 +9192,9 @@ var HARNESSES = [
|
|
|
9073
9192
|
layout: AGENTS_LAYOUT,
|
|
9074
9193
|
rule: "block",
|
|
9075
9194
|
markers: [".cursor"],
|
|
9076
|
-
|
|
9195
|
+
// @decision(DL-061)
|
|
9196
|
+
instructions: ["AGENTS.md", "CLAUDE.md"],
|
|
9197
|
+
readsAll: true
|
|
9077
9198
|
},
|
|
9078
9199
|
{
|
|
9079
9200
|
name: "opencode",
|
|
@@ -9081,7 +9202,8 @@ var HARNESSES = [
|
|
|
9081
9202
|
layout: AGENTS_LAYOUT,
|
|
9082
9203
|
rule: "block",
|
|
9083
9204
|
markers: [".opencode", "opencode.json", "opencode.jsonc"],
|
|
9084
|
-
|
|
9205
|
+
// @decision(DL-061) OpenCode 2.x; 1.x also fell back to CLAUDE.md.
|
|
9206
|
+
instructions: ["AGENTS.md"]
|
|
9085
9207
|
},
|
|
9086
9208
|
{
|
|
9087
9209
|
name: "pi",
|
|
@@ -9089,7 +9211,7 @@ var HARNESSES = [
|
|
|
9089
9211
|
layout: AGENTS_LAYOUT,
|
|
9090
9212
|
rule: "block",
|
|
9091
9213
|
markers: [".pi"],
|
|
9092
|
-
instructions:
|
|
9214
|
+
instructions: ["AGENTS.md", "CLAUDE.md"]
|
|
9093
9215
|
}
|
|
9094
9216
|
];
|
|
9095
9217
|
var GENERIC_MARKERS = ["AGENTS.md", ".agents/skills"];
|
|
@@ -9256,20 +9378,23 @@ function removeEmptyParents(ctx, dir, paths) {
|
|
|
9256
9378
|
}
|
|
9257
9379
|
|
|
9258
9380
|
// src/generate/rule.ts
|
|
9259
|
-
import { dirname as dirname3, join as join15 } from "node:path";
|
|
9381
|
+
import { dirname as dirname3, join as join15, posix as posix4 } from "node:path";
|
|
9260
9382
|
|
|
9261
9383
|
// templates/rules/dld-workflow.md
|
|
9262
|
-
var dld_workflow_default = "# DLD (Decision-Linked Development)\n\nThis project uses Decision-Linked Development. Decision records (DL-*.md) live in the `records/` subdirectory of the decisions directory set in `dld.config.yaml` (`decisions/` by default). High-level docs (INDEX.md, OVERVIEW.md, SNAPSHOT.md) live in the decisions directory.\n\n## Rules\n\n- When you encounter `@decision(DL-XXX)` annotations in code, read the referenced decision with the dld-lookup skill BEFORE modifying the annotated code.\n- ALWAYS look up and verify related decisions before modifying annotated code. Do not skip this step.\n- NEVER modify code in a way that contradicts an existing decision without first confirming with the user. If the change requires breaking a previous decision, a new decision must be recorded (with the dld-decide skill) that explicitly supersedes the old one. If it only partially modifies a previous decision, record it as an amendment instead.\n\n## Skills\n\n- dld-decide: record a new decision\n- dld-plan: break down a feature into multiple grouped decisions\n- dld-implement: implement proposed decisions\n- dld-lookup: query decisions by ID, tag, or code path\n- dld-adjust: adjust or update existing decisions\n- dld-audit: scan for drift between decisions and code\n- dld-snapshot: regenerate SNAPSHOT.md and OVERVIEW.md from the decision log\n- dld-status: a quick overview of the decision log state\n- dld-retrofit: generate decisions from an existing codebase\n- dld-reindex: resolve decision-ID collisions with the base branch
|
|
9384
|
+
var dld_workflow_default = "# DLD (Decision-Linked Development)\n\nThis project uses Decision-Linked Development. Decision records (DL-*.md) live in the `records/` subdirectory of the decisions directory set in `dld.config.yaml` (`decisions/` by default). High-level docs (INDEX.md, OVERVIEW.md, SNAPSHOT.md) live in the decisions directory.\n\n## Rules\n\n- When you encounter `@decision(DL-XXX)` annotations in code, read the referenced decision with the dld-lookup skill BEFORE modifying the annotated code.\n- ALWAYS look up and verify related decisions before modifying annotated code. Do not skip this step.\n- NEVER modify code in a way that contradicts an existing decision without first confirming with the user. If the change requires breaking a previous decision, a new decision must be recorded (with the dld-decide skill) that explicitly supersedes the old one. If it only partially modifies a previous decision, record it as an amendment instead.\n\n## Skills\n\n- dld-decide: record a new decision\n- dld-plan: break down a feature into multiple grouped decisions\n- dld-implement: implement proposed decisions\n- dld-lookup: query decisions by ID, tag, or code path\n- dld-adjust: adjust or update existing decisions\n- dld-audit: scan for drift between decisions and code\n- dld-snapshot: regenerate SNAPSHOT.md and OVERVIEW.md from the decision log\n- dld-status: a quick overview of the decision log state\n- dld-retrofit: generate decisions from an existing codebase\n- dld-reindex: resolve decision-ID collisions with the base branch and open PRs\n";
|
|
9263
9385
|
|
|
9264
9386
|
// src/generate/rule.ts
|
|
9265
9387
|
var RULE_TEXT = dld_workflow_default;
|
|
9266
9388
|
var RULE_SOURCE = "templates/rules/dld-workflow.md";
|
|
9267
9389
|
var CLAUDE_RULE_FILE = ".claude/rules/dld-workflow.md";
|
|
9268
9390
|
var AGENTS_RULE_FILE = ".agents/rules/dld-workflow.md";
|
|
9391
|
+
var CLAUDE_IMPORT_FILE = ".claude/CLAUDE.md";
|
|
9269
9392
|
var BLOCK_START = "<!-- dld-kit:start -->";
|
|
9270
9393
|
var BLOCK_END = "<!-- dld-kit:end -->";
|
|
9271
9394
|
var AGENTS_MD = "AGENTS.md";
|
|
9272
9395
|
var CLAUDE_MD = "CLAUDE.md";
|
|
9396
|
+
var CLAUDE_FILES = [CLAUDE_MD, CLAUDE_IMPORT_FILE, "CLAUDE.local.md"];
|
|
9397
|
+
var SHARED_CLAUDE_FILES = [CLAUDE_MD, CLAUDE_IMPORT_FILE];
|
|
9273
9398
|
var RULE_FILES = {
|
|
9274
9399
|
"claude-file": CLAUDE_RULE_FILE,
|
|
9275
9400
|
"agents-file": AGENTS_RULE_FILE
|
|
@@ -9287,6 +9412,16 @@ function renderRuleFile(channel, text, version2) {
|
|
|
9287
9412
|
|
|
9288
9413
|
${text}`;
|
|
9289
9414
|
}
|
|
9415
|
+
function renderImportFile(version2) {
|
|
9416
|
+
return `<!-- Generated by dld-kit ${version2} for Claude Code: it loads AGENTS.md, which holds the DLD rule, through the import below. Do not edit: updating the rule overwrites this file. -->
|
|
9417
|
+
|
|
9418
|
+
@../AGENTS.md
|
|
9419
|
+
`;
|
|
9420
|
+
}
|
|
9421
|
+
var IMPORT_STAMP = /^<!-- Generated by dld-kit (\S+) for Claude Code:/;
|
|
9422
|
+
function importFileVersion(content) {
|
|
9423
|
+
return IMPORT_STAMP.exec(content)?.[1];
|
|
9424
|
+
}
|
|
9290
9425
|
function renderBlock(text, version2) {
|
|
9291
9426
|
const body = text.endsWith("\n") ? text.slice(0, -1) : text;
|
|
9292
9427
|
return [BLOCK_START, notice(version2, "block"), "", ...body.split("\n"), BLOCK_END];
|
|
@@ -9335,20 +9470,41 @@ function planRule(ctx, root, channels, version2, { harnesses = [] } = {}, text =
|
|
|
9335
9470
|
let blockFiles = [AGENTS_MD, CLAUDE_MD].filter(
|
|
9336
9471
|
(file) => regular(file) && findBlock(read(file), file) !== void 0
|
|
9337
9472
|
);
|
|
9338
|
-
if (blockFiles.length === 0 && channels.has("block"))
|
|
9473
|
+
if (blockFiles.length === 0 && channels.has("block")) {
|
|
9474
|
+
const targets = harnesses.filter((h) => h.rule === "block");
|
|
9475
|
+
const file = blockPlacement(ctx, root, targets);
|
|
9476
|
+
blockFiles = [file];
|
|
9477
|
+
plan.warnings.push(...switchWarnings(ctx, root, file, targets));
|
|
9478
|
+
}
|
|
9479
|
+
const pending = /* @__PURE__ */ new Map();
|
|
9339
9480
|
for (const file of blockFiles) {
|
|
9340
9481
|
const content = read(file);
|
|
9341
9482
|
const updated = upsertBlock(content, renderBlock(text, version2), file);
|
|
9483
|
+
pending.set(file, updated);
|
|
9342
9484
|
if (updated !== content) plan.writes.push({ path: file, content: updated });
|
|
9343
9485
|
}
|
|
9486
|
+
const linkedDir = symlinkedPart(ctx, root, dirname3(CLAUDE_IMPORT_FILE));
|
|
9487
|
+
const ownsImport = linkedDir === void 0 && importFileVersion(read(CLAUDE_IMPORT_FILE)) !== void 0;
|
|
9488
|
+
const wantsImport = channels.has("claude-file") && blockFiles.includes(AGENTS_MD) && !SHARED_CLAUDE_FILES.some((file) => ctx.fs.lexists(join15(root, file)));
|
|
9489
|
+
if (ownsImport || wantsImport && linkedDir === void 0) {
|
|
9490
|
+
const content = renderImportFile(version2);
|
|
9491
|
+
pending.set(CLAUDE_IMPORT_FILE, content);
|
|
9492
|
+
if (read(CLAUDE_IMPORT_FILE) !== content) {
|
|
9493
|
+
plan.writes.push({ path: CLAUDE_IMPORT_FILE, content });
|
|
9494
|
+
}
|
|
9495
|
+
} else if (wantsImport) {
|
|
9496
|
+
plan.warnings.push(
|
|
9497
|
+
`${linkedDir} is a symlink, so dld-kit does not write ${CLAUDE_IMPORT_FILE}, which would import AGENTS.md for Claude Code. Claude Code reads the rule block in AGENTS.md by itself only while the project has no CLAUDE.md, ${CLAUDE_IMPORT_FILE} or CLAUDE.local.md.`
|
|
9498
|
+
);
|
|
9499
|
+
}
|
|
9344
9500
|
const withBlock = new Set(blockFiles.map(resolved));
|
|
9345
|
-
const
|
|
9346
|
-
const
|
|
9347
|
-
|
|
9348
|
-
|
|
9349
|
-
const file = reads(harness);
|
|
9350
|
-
return file !== void 0 && loads(file);
|
|
9501
|
+
const current = projectView(ctx, root);
|
|
9502
|
+
const view = {
|
|
9503
|
+
exists: (file) => pending.has(file) || current.exists(file),
|
|
9504
|
+
read: (file) => pending.get(file) ?? current.read(file)
|
|
9351
9505
|
};
|
|
9506
|
+
const reads = (harness) => instructionFiles(harness, view)[0];
|
|
9507
|
+
const readsBlock = (harness) => instructionFiles(harness, view).some((file) => withBlock.has(resolved(file)));
|
|
9352
9508
|
const ownerOf = (channel) => HARNESSES.find((h) => h.rule === channel);
|
|
9353
9509
|
const loadsBlock = (channel) => {
|
|
9354
9510
|
const owner = ownerOf(channel);
|
|
@@ -9362,11 +9518,6 @@ function planRule(ctx, root, channels, version2, { harnesses = [] } = {}, text =
|
|
|
9362
9518
|
reads
|
|
9363
9519
|
)
|
|
9364
9520
|
);
|
|
9365
|
-
const claude = harnesses.find((h) => h.rule === "claude-file");
|
|
9366
|
-
const blockWritten = plan.writes.some((write) => write.path === AGENTS_MD);
|
|
9367
|
-
if (claude !== void 0 && reads(claude) === AGENTS_MD && readsBlock(claude) && blockWritten) {
|
|
9368
|
-
plan.warnings.push(CLAUDE_READS_AGENTS_NOTE);
|
|
9369
|
-
}
|
|
9370
9521
|
}
|
|
9371
9522
|
for (const channel of ["claude-file", "agents-file"]) {
|
|
9372
9523
|
const path = RULE_FILES[channel];
|
|
@@ -9382,8 +9533,45 @@ function planRule(ctx, root, channels, version2, { harnesses = [] } = {}, text =
|
|
|
9382
9533
|
plan.warnings.push(...legacyBlockWarnings(read(CLAUDE_MD), ruleInstalled));
|
|
9383
9534
|
return plan;
|
|
9384
9535
|
}
|
|
9385
|
-
function
|
|
9386
|
-
|
|
9536
|
+
function projectView(ctx, root) {
|
|
9537
|
+
const exists = (file) => ctx.fs.exists(join15(root, file));
|
|
9538
|
+
return {
|
|
9539
|
+
exists,
|
|
9540
|
+
read: (file) => exists(file) && !ctx.fs.isDirectory(join15(root, file)) ? ctx.fs.readFile(join15(root, file)) : ""
|
|
9541
|
+
};
|
|
9542
|
+
}
|
|
9543
|
+
function instructionFiles(harness, view) {
|
|
9544
|
+
if (harness.name !== "claude") {
|
|
9545
|
+
if (harness.readsAll) return harness.instructions.filter((f) => view.exists(f));
|
|
9546
|
+
const file = harness.instructions.find((f) => view.exists(f));
|
|
9547
|
+
return file === void 0 ? [] : [file];
|
|
9548
|
+
}
|
|
9549
|
+
const files = CLAUDE_FILES.filter((file) => view.exists(file));
|
|
9550
|
+
if (files.length === 0) return view.exists(AGENTS_MD) ? [AGENTS_MD] : [];
|
|
9551
|
+
const imported = files.some((file) => importsAgentsMd(view.read(file), file));
|
|
9552
|
+
return imported && view.exists(AGENTS_MD) ? [...files, AGENTS_MD] : files;
|
|
9553
|
+
}
|
|
9554
|
+
var FENCE = /^ {0,3}(`{3,}|~{3,})/;
|
|
9555
|
+
function importsAgentsMd(content, file) {
|
|
9556
|
+
const dir = posix4.dirname(file);
|
|
9557
|
+
let fence;
|
|
9558
|
+
for (const line of content.split(/\r?\n/)) {
|
|
9559
|
+
const marker = FENCE.exec(line)?.[1];
|
|
9560
|
+
if (marker !== void 0) {
|
|
9561
|
+
if (fence === void 0) fence = marker;
|
|
9562
|
+
else if (marker[0] === fence[0] && marker.length >= fence.length) fence = void 0;
|
|
9563
|
+
continue;
|
|
9564
|
+
}
|
|
9565
|
+
if (fence !== void 0) continue;
|
|
9566
|
+
const prose = line.replace(/`[^`]*`/g, "");
|
|
9567
|
+
for (const match of prose.matchAll(/(?:^|\s)@(\S+)/g)) {
|
|
9568
|
+
const target = match[1];
|
|
9569
|
+
if (target !== void 0 && posix4.normalize(posix4.join(dir, target)) === AGENTS_MD) {
|
|
9570
|
+
return true;
|
|
9571
|
+
}
|
|
9572
|
+
}
|
|
9573
|
+
}
|
|
9574
|
+
return false;
|
|
9387
9575
|
}
|
|
9388
9576
|
function blindWarnings(blockFiles, blind, reads) {
|
|
9389
9577
|
const byFile = /* @__PURE__ */ new Map();
|
|
@@ -9397,18 +9585,30 @@ function blindWarnings(blockFiles, blind, reads) {
|
|
|
9397
9585
|
return `The dld-kit rule block is in ${blockFiles.join(" and ")}, but ${names} ${one ? "reads" : "read"} ${file}. To cover ${one ? "it" : "them"}, move the block (the dld-kit:start line through the dld-kit:end line) into ${file}, then run dld install-rule.`;
|
|
9398
9586
|
});
|
|
9399
9587
|
}
|
|
9400
|
-
|
|
9401
|
-
|
|
9402
|
-
|
|
9403
|
-
|
|
9588
|
+
function blockPlacement(ctx, root, targets) {
|
|
9589
|
+
if (ctx.fs.isRegularFile(join15(root, AGENTS_MD))) return AGENTS_MD;
|
|
9590
|
+
const agentsLinked = ctx.fs.lexists(join15(root, AGENTS_MD));
|
|
9591
|
+
if (ctx.fs.isRegularFile(join15(root, CLAUDE_MD)) && (agentsLinked || targets.every((h) => h.instructions.includes(CLAUDE_MD)))) {
|
|
9592
|
+
return CLAUDE_MD;
|
|
9404
9593
|
}
|
|
9405
|
-
if (
|
|
9594
|
+
if (agentsLinked) {
|
|
9406
9595
|
throw new DldError(
|
|
9407
9596
|
`${AGENTS_MD} is not a regular file (a symlink?); dld-kit does not edit through it. Point it at a regular file or add the dld-kit block by hand.`
|
|
9408
9597
|
);
|
|
9409
9598
|
}
|
|
9410
9599
|
return AGENTS_MD;
|
|
9411
9600
|
}
|
|
9601
|
+
function switchWarnings(ctx, root, file, targets) {
|
|
9602
|
+
if (file !== AGENTS_MD || ctx.fs.lexists(join15(root, AGENTS_MD))) return [];
|
|
9603
|
+
const claudeMd = join15(root, CLAUDE_MD);
|
|
9604
|
+
if (!ctx.fs.exists(claudeMd) || ctx.fs.isDirectory(claudeMd)) return [];
|
|
9605
|
+
const switching = targets.filter((h) => !h.readsAll && h.instructions.includes(CLAUDE_MD));
|
|
9606
|
+
if (switching.length === 0) return [];
|
|
9607
|
+
const names = switching.map((h) => h.title).join(" and ");
|
|
9608
|
+
return [
|
|
9609
|
+
`dld-kit puts the rule block in a new AGENTS.md. ${names} will read AGENTS.md instead of CLAUDE.md from now on, so move anything ${switching.length === 1 ? "it needs" : "they need"} from CLAUDE.md into AGENTS.md.`
|
|
9610
|
+
];
|
|
9611
|
+
}
|
|
9412
9612
|
var LEGACY_HEADING = /^## DLD \(Decision-Linked Development\)\s*$/;
|
|
9413
9613
|
function legacyBlockWarnings(claudeMd, ruleInstalled) {
|
|
9414
9614
|
const span = findBlock(claudeMd, CLAUDE_MD);
|
|
@@ -9423,32 +9623,43 @@ function loadsRule(ctx, root, harness) {
|
|
|
9423
9623
|
if (harness.rule !== "block" && ctx.fs.exists(join15(root, RULE_FILES[harness.rule]))) {
|
|
9424
9624
|
return true;
|
|
9425
9625
|
}
|
|
9426
|
-
const
|
|
9427
|
-
|
|
9428
|
-
|
|
9626
|
+
const view = projectView(ctx, root);
|
|
9627
|
+
return instructionFiles(harness, view).some(
|
|
9628
|
+
(file) => view.read(file).split(/\r?\n/).some((line) => line === BLOCK_START || LEGACY_HEADING.test(line))
|
|
9629
|
+
);
|
|
9429
9630
|
}
|
|
9430
9631
|
function sessionContext(ctx, root, harness, text = RULE_TEXT) {
|
|
9431
9632
|
if (!ctx.fs.exists(join15(root, CONFIG_FILE))) return void 0;
|
|
9432
9633
|
return loadsRule(ctx, root, harness) ? void 0 : text;
|
|
9433
9634
|
}
|
|
9434
|
-
function
|
|
9635
|
+
function symlinkedPart(ctx, root, dir) {
|
|
9435
9636
|
let current = "";
|
|
9436
9637
|
for (const part of dir.split("/")) {
|
|
9437
9638
|
current = current === "" ? part : `${current}/${part}`;
|
|
9438
9639
|
const full = join15(root, current);
|
|
9439
|
-
if (!ctx.fs.lexists(full)) return;
|
|
9640
|
+
if (!ctx.fs.lexists(full)) return void 0;
|
|
9440
9641
|
if (!ctx.fs.exists(full) || ctx.fs.realPath(full) !== join15(ctx.fs.realPath(root), current)) {
|
|
9441
|
-
|
|
9442
|
-
`${current} is a symlink; dld-kit does not install through symlinks. Replace it with a directory (dld-kit keeps each agent's copy separate).`
|
|
9443
|
-
);
|
|
9642
|
+
return current;
|
|
9444
9643
|
}
|
|
9445
9644
|
}
|
|
9645
|
+
return void 0;
|
|
9646
|
+
}
|
|
9647
|
+
function refuseSymlinkedDir(ctx, root, dir) {
|
|
9648
|
+
const linked = symlinkedPart(ctx, root, dir);
|
|
9649
|
+
if (linked !== void 0) {
|
|
9650
|
+
throw new DldError(
|
|
9651
|
+
`${linked} is a symlink; dld-kit does not install through symlinks. Replace it with a directory (dld-kit keeps each agent's copy separate).`
|
|
9652
|
+
);
|
|
9653
|
+
}
|
|
9446
9654
|
}
|
|
9447
9655
|
function installedRuleChannels(ctx, root) {
|
|
9448
9656
|
const channels = /* @__PURE__ */ new Set();
|
|
9449
9657
|
for (const channel of ["claude-file", "agents-file"]) {
|
|
9450
9658
|
if (ctx.fs.lexists(join15(root, RULE_FILES[channel]))) channels.add(channel);
|
|
9451
9659
|
}
|
|
9660
|
+
if (importFileVersion(projectView(ctx, root).read(CLAUDE_IMPORT_FILE)) !== void 0) {
|
|
9661
|
+
channels.add("claude-file");
|
|
9662
|
+
}
|
|
9452
9663
|
for (const file of [AGENTS_MD, CLAUDE_MD]) {
|
|
9453
9664
|
const path = join15(root, file);
|
|
9454
9665
|
if (ctx.fs.isRegularFile(path) && findBlock(ctx.fs.readFile(path), file) !== void 0) {
|
|
@@ -9463,6 +9674,8 @@ function installedRuleStamps(ctx, root) {
|
|
|
9463
9674
|
for (const file of [CLAUDE_RULE_FILE, AGENTS_RULE_FILE].filter(regular)) {
|
|
9464
9675
|
stamps.push({ path: file, version: ruleVersion(ctx.fs.readFile(join15(root, file))) });
|
|
9465
9676
|
}
|
|
9677
|
+
const imported = importFileVersion(projectView(ctx, root).read(CLAUDE_IMPORT_FILE));
|
|
9678
|
+
if (imported !== void 0) stamps.push({ path: CLAUDE_IMPORT_FILE, version: imported });
|
|
9466
9679
|
for (const file of [AGENTS_MD, CLAUDE_MD].filter(regular)) {
|
|
9467
9680
|
const content = ctx.fs.readFile(join15(root, file));
|
|
9468
9681
|
const span = findBlock(content, file);
|
|
@@ -9705,8 +9918,13 @@ function printReport(io, report2) {
|
|
|
9705
9918
|
io.stdout(`${dir}: ${written} written, ${removed} removed, ${unchanged} unchanged
|
|
9706
9919
|
`);
|
|
9707
9920
|
}
|
|
9708
|
-
for (const path of report2.ruleWritten)
|
|
9709
|
-
|
|
9921
|
+
for (const path of report2.ruleWritten) {
|
|
9922
|
+
io.stdout(
|
|
9923
|
+
path === CLAUDE_IMPORT_FILE ? `Wrote ${path} (imports AGENTS.md for Claude Code)
|
|
9924
|
+
` : `Wrote the DLD rule to ${path}
|
|
9925
|
+
`
|
|
9926
|
+
);
|
|
9927
|
+
}
|
|
9710
9928
|
for (const path of report2.ruleRemoved) io.stdout(`Removed ${path} (the rule is in the block)
|
|
9711
9929
|
`);
|
|
9712
9930
|
for (const warning of report2.warnings) io.stderr(`Warning: ${warning}
|
|
@@ -9835,6 +10053,7 @@ ${AGENT_HELP}
|
|
|
9835
10053
|
var listTakenIdsCommand = {
|
|
9836
10054
|
name: "list-taken-ids",
|
|
9837
10055
|
summary: "List decision IDs taken on the base branch and in open PRs",
|
|
10056
|
+
internal: true,
|
|
9838
10057
|
usage: `Usage: dld list-taken-ids [--base <ref>]
|
|
9839
10058
|
|
|
9840
10059
|
Print the decision IDs on the base branch and in records touched by open pull requests
|
|
@@ -9858,6 +10077,7 @@ Options:
|
|
|
9858
10077
|
var nextIdCommand = {
|
|
9859
10078
|
name: "next-id",
|
|
9860
10079
|
summary: "Print the next sequential decision ID",
|
|
10080
|
+
internal: true,
|
|
9861
10081
|
usage: "Usage: dld next-id\n\nPrint the next decision ID, e.g. DL-004.\n",
|
|
9862
10082
|
run(args, io, ctx) {
|
|
9863
10083
|
parseCommandArgs({ args: [...args], options: {} });
|
|
@@ -9872,6 +10092,7 @@ var nextIdCommand = {
|
|
|
9872
10092
|
var planRenamesCommand = {
|
|
9873
10093
|
name: "plan-renames",
|
|
9874
10094
|
summary: "Plan renames that resolve decision ID collisions",
|
|
10095
|
+
internal: true,
|
|
9875
10096
|
usage: `Usage: dld plan-renames [--base <ref>]
|
|
9876
10097
|
|
|
9877
10098
|
Print <path>\\t<DL-OLD>\\t<DL-NEW> for each colliding local decision, assigning the next free
|
|
@@ -9895,6 +10116,7 @@ Options:
|
|
|
9895
10116
|
var regenerateIndexCommand = {
|
|
9896
10117
|
name: "regenerate-index",
|
|
9897
10118
|
summary: "Rebuild INDEX.md from the decision records",
|
|
10119
|
+
internal: true,
|
|
9898
10120
|
usage: `Usage: dld regenerate-index [--include-base <ref>]
|
|
9899
10121
|
|
|
9900
10122
|
Rebuild INDEX.md from every decision record.
|
|
@@ -9922,6 +10144,7 @@ Options:
|
|
|
9922
10144
|
var renameDecisionCommand = {
|
|
9923
10145
|
name: "rename-decision",
|
|
9924
10146
|
summary: "Rename a local decision and rewrite its references",
|
|
10147
|
+
internal: true,
|
|
9925
10148
|
usage: `Usage: dld rename-decision --old <DL-OLD> --new <DL-NEW> --path <path> [--base <ref>]
|
|
9926
10149
|
|
|
9927
10150
|
Rename a locally added decision with git mv, rewrite its id and references in changed
|
|
@@ -9957,6 +10180,7 @@ Options:
|
|
|
9957
10180
|
var resolveBaseCommand = {
|
|
9958
10181
|
name: "resolve-base",
|
|
9959
10182
|
summary: "Print the base ref to check decision IDs against",
|
|
10183
|
+
internal: true,
|
|
9960
10184
|
usage: `Usage: dld resolve-base
|
|
9961
10185
|
|
|
9962
10186
|
Print the current branch's upstream when it tracks a differently named branch, otherwise
|
|
@@ -10068,11 +10292,12 @@ ${AGENT_HELP}
|
|
|
10068
10292
|
var updateAuditStateCommand = {
|
|
10069
10293
|
name: "update-audit-state",
|
|
10070
10294
|
summary: "Record the audit run in .dld-state.yaml",
|
|
10295
|
+
internal: true,
|
|
10071
10296
|
usage: "Usage: dld update-audit-state\n\nRecord the current time and HEAD commit as the last audit.\n",
|
|
10072
10297
|
run(args, io, ctx) {
|
|
10073
10298
|
parseCommandArgs({ args: [...args], options: {} });
|
|
10074
|
-
const { timestamp, commit } = updateAuditState(ctx, loadProject(ctx));
|
|
10075
|
-
io.stdout(`Audit state updated: ${timestamp} at ${
|
|
10299
|
+
const { timestamp, commit: commit2 } = updateAuditState(ctx, loadProject(ctx));
|
|
10300
|
+
io.stdout(`Audit state updated: ${timestamp} at ${commit2}
|
|
10076
10301
|
`);
|
|
10077
10302
|
return EXIT_OK;
|
|
10078
10303
|
}
|
|
@@ -10082,6 +10307,7 @@ var updateAuditStateCommand = {
|
|
|
10082
10307
|
var updateSnapshotStateCommand = {
|
|
10083
10308
|
name: "update-snapshot-state",
|
|
10084
10309
|
summary: "Record the snapshot run in .dld-state.yaml",
|
|
10310
|
+
internal: true,
|
|
10085
10311
|
usage: `Usage: dld update-snapshot-state [artifact ...]
|
|
10086
10312
|
|
|
10087
10313
|
Record the snapshot time, HEAD commit and highest accepted decision, with timestamps for
|
|
@@ -10093,8 +10319,8 @@ SNAPSHOT.md, OVERVIEW.md and any custom artifacts named.
|
|
|
10093
10319
|
options: {},
|
|
10094
10320
|
allowPositionals: true
|
|
10095
10321
|
});
|
|
10096
|
-
const { timestamp, commit, highest } = updateSnapshotState(ctx, loadProject(ctx), positionals);
|
|
10097
|
-
io.stdout(`Snapshot state updated: ${timestamp} at ${
|
|
10322
|
+
const { timestamp, commit: commit2, highest } = updateSnapshotState(ctx, loadProject(ctx), positionals);
|
|
10323
|
+
io.stdout(`Snapshot state updated: ${timestamp} at ${commit2} (through ${formatId(highest)})
|
|
10098
10324
|
`);
|
|
10099
10325
|
return EXIT_OK;
|
|
10100
10326
|
}
|
|
@@ -10104,6 +10330,7 @@ SNAPSHOT.md, OVERVIEW.md and any custom artifacts named.
|
|
|
10104
10330
|
var updateStatusCommand = {
|
|
10105
10331
|
name: "update-status",
|
|
10106
10332
|
summary: "Set a decision's status",
|
|
10333
|
+
internal: true,
|
|
10107
10334
|
usage: `Usage: dld update-status <DL-NNN> <${STATUSES.join("|")}>
|
|
10108
10335
|
|
|
10109
10336
|
Change only the status line of a decision record.
|
|
@@ -10135,6 +10362,7 @@ var EXIT_MISSING = 1;
|
|
|
10135
10362
|
var verifyAnnotationsCommand = {
|
|
10136
10363
|
name: "verify-annotations",
|
|
10137
10364
|
summary: "Check that decisions have annotations in the code",
|
|
10365
|
+
internal: true,
|
|
10138
10366
|
usage: "Usage: dld verify-annotations <DL-NNN> [DL-NNN ...]\n\nExit 0 if every decision has at least one annotation, 1 if any are missing.\n",
|
|
10139
10367
|
run(args, io, ctx) {
|
|
10140
10368
|
const { positionals: ids } = parseCommandArgs({
|
|
@@ -10188,11 +10416,19 @@ var COMMANDS = [
|
|
|
10188
10416
|
findStaleMentionsCommand,
|
|
10189
10417
|
commitReindexCommand
|
|
10190
10418
|
];
|
|
10419
|
+
var INTERNAL_HEADING = "Commands the skills run (internal; may change in minor releases)";
|
|
10420
|
+
var INTERNAL_NOTE = "Internal: the dld-kit skills run this command. Its name, arguments and output may change in a minor release.\n";
|
|
10191
10421
|
function usage(commands) {
|
|
10422
|
+
const list = (group) => group.map((c) => ` ${c.name.padEnd(25)} ${c.summary}`).join("\n");
|
|
10423
|
+
const setup = commands.filter((c) => !c.internal);
|
|
10424
|
+
const internal = commands.filter((c) => c.internal);
|
|
10425
|
+
const sections = [`Setup commands:
|
|
10426
|
+
${list(setup)}`];
|
|
10427
|
+
if (internal.length > 0) sections.push(`${INTERNAL_HEADING}:
|
|
10428
|
+
${list(internal)}`);
|
|
10192
10429
|
return `Usage: dld <command> [options]
|
|
10193
10430
|
|
|
10194
|
-
|
|
10195
|
-
${commands.map((c) => ` ${c.name.padEnd(20)} ${c.summary}`).join("\n")}
|
|
10431
|
+
${sections.join("\n\n")}
|
|
10196
10432
|
|
|
10197
10433
|
Options:
|
|
10198
10434
|
-h, --help Show this help, or a command's help after its name
|
|
@@ -10231,7 +10467,8 @@ ${usage(commands)}`);
|
|
|
10231
10467
|
}
|
|
10232
10468
|
function report(error, command, io) {
|
|
10233
10469
|
if (error instanceof HelpRequested) {
|
|
10234
|
-
io.stdout(command.usage
|
|
10470
|
+
io.stdout(command.internal ? `${command.usage}
|
|
10471
|
+
${INTERNAL_NOTE}` : command.usage);
|
|
10235
10472
|
return EXIT_OK;
|
|
10236
10473
|
}
|
|
10237
10474
|
if (error instanceof UsageError) {
|