etymd 0.15.0 → 0.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -30,7 +30,7 @@ var init_package = __esm({
30
30
  "package.json"() {
31
31
  package_default = {
32
32
  name: "etymd",
33
- version: "0.15.0",
33
+ version: "0.16.0",
34
34
  description: "Keep your agent instructions true \u2014 verify AGENTS.md, CLAUDE.md, rules, skills, and the task you hand an agent against the actual repo, with drift caught over time and a regression ledger.",
35
35
  keywords: [
36
36
  "cli",
@@ -49,11 +49,11 @@ var init_package = __esm({
49
49
  author: "Alkis Yuv",
50
50
  repository: {
51
51
  type: "git",
52
- url: "git+https://github.com/triartleet/etymd.git"
52
+ url: "git+https://github.com/fleetorders/etymd.git"
53
53
  },
54
- homepage: "https://github.com/triartleet/etymd#readme",
54
+ homepage: "https://github.com/fleetorders/etymd#readme",
55
55
  bugs: {
56
- url: "https://github.com/triartleet/etymd/issues"
56
+ url: "https://github.com/fleetorders/etymd/issues"
57
57
  },
58
58
  type: "module",
59
59
  bin: {
@@ -1171,7 +1171,7 @@ function generatePreCommitHook(selfBuild = false) {
1171
1171
 
1172
1172
  ${localHookCall("pre-commit")}
1173
1173
 
1174
- # Content screen \u2014 staged file bytes. Refuses to commit environment, employer or identity
1174
+ # Content screen \u2014 staged file bytes. Refuses to commit environment, guarded-side or identity
1175
1175
  # detail into a repo whose history is (or could become) public. The checker and its patterns
1176
1176
  # are machine-local by design, so this is a NO-OP wherever no checker is installed: safe to
1177
1177
  # commit anywhere, active only where you opted in.
@@ -2762,6 +2762,13 @@ var PLACEHOLDER_PREFIX_RE = /^(?:my|your)-/i;
2762
2762
  function isPlaceholderClaim(token) {
2763
2763
  return token.split("/").some((seg) => PLACEHOLDER_PREFIX_RE.test(seg) || PLACEHOLDER_SEGMENTS.has(seg.toLowerCase()));
2764
2764
  }
2765
+ var NAMESPACE_IDENT = "[A-Za-z][A-Za-z0-9-]{0,15}";
2766
+ var NAMESPACE_STOP = new Set(
2767
+ "note notes see warning caution caveat caveats example examples ex eg ie nb ps re per file files path paths hint tip tips todo fixme step steps rule rules ref refs fact facts output outputs input inputs result results summary status overview next prev then thus hence plus goal goals spec specs context".split(" ")
2768
+ );
2769
+ function isNamespace(ident) {
2770
+ return Boolean(ident) && !NAMESPACE_STOP.has(ident.toLowerCase());
2771
+ }
2765
2772
  function claimContext(text, index) {
2766
2773
  const start = text.lastIndexOf("\n", index) + 1;
2767
2774
  const endRaw = text.indexOf("\n", index);
@@ -2781,8 +2788,9 @@ ${line}`;
2781
2788
  }
2782
2789
  return line;
2783
2790
  }
2784
- function extractPathClaims(text) {
2791
+ function extractPathClaims(text, opts = {}) {
2785
2792
  const prospectiveOnly = /* @__PURE__ */ new Map();
2793
+ const namespacedOnly = /* @__PURE__ */ new Map();
2786
2794
  const placeholder = /* @__PURE__ */ new Set();
2787
2795
  for (const m of text.matchAll(/`([^`\n]+)`/g)) {
2788
2796
  const token = m[1].trim();
@@ -2804,14 +2812,21 @@ function extractPathClaims(text) {
2804
2812
  }
2805
2813
  const prospective2 = CREATION_CONTEXT_RE.test(claimContext(text, m.index ?? 0));
2806
2814
  prospectiveOnly.set(claim, (prospectiveOnly.get(claim) ?? true) && prospective2);
2815
+ if (opts.namespaces) {
2816
+ const nsLead = new RegExp(`(${NAMESPACE_IDENT}):[ \\t]*$`).exec(text.slice(0, m.index ?? 0));
2817
+ const prefixed = isNamespace(nsLead?.[1]);
2818
+ namespacedOnly.set(claim, (namespacedOnly.get(claim) ?? true) && prefixed);
2819
+ }
2807
2820
  }
2808
2821
  const paths = [];
2809
2822
  const prospective = [];
2823
+ const namespaced = [];
2810
2824
  for (const [claim, only] of prospectiveOnly) {
2811
2825
  if (only) prospective.push(claim);
2826
+ else if (namespacedOnly.get(claim)) namespaced.push(claim);
2812
2827
  else paths.push(claim);
2813
2828
  }
2814
- return { paths, prospective, placeholder: [...placeholder] };
2829
+ return { paths, prospective, namespaced, placeholder: [...placeholder] };
2815
2830
  }
2816
2831
  var LOCAL_REF_LEADINS = new Set(
2817
2832
  "decision decisions entry entries ruling rulings record records ledger id ids item items see per in of on at by to as is was are were the a an and or but not with under over from via vs than after before since between through against latest newest earliest only also still now supersedes superseded superseding amends amended extends extended cites cited citing adds added adding wrote written writes locked locks closed closes opened opens resolves resolved reopened recorded number numbers".split(" ")
@@ -2920,7 +2935,9 @@ function emptyCounters() {
2920
2935
  prospectiveSkipped: 0,
2921
2936
  placeholderSkipped: 0,
2922
2937
  qualifiedRefsSkipped: 0,
2923
- unresolvableRefs: 0
2938
+ unresolvableRefs: 0,
2939
+ namespacedSkipped: 0,
2940
+ outsideRepoSkipped: 0
2924
2941
  };
2925
2942
  }
2926
2943
  async function checkTextClaims(env, file, opts, counters) {
@@ -2958,9 +2975,12 @@ async function checkTextClaims(env, file, opts, counters) {
2958
2975
  confidence: "high"
2959
2976
  });
2960
2977
  }
2961
- const { paths, prospective, placeholder } = extractPathClaims(file.text);
2978
+ const { paths, prospective, placeholder, namespaced } = extractPathClaims(file.text, {
2979
+ namespaces: opts.treatNamespacedPrefixes
2980
+ });
2962
2981
  counters.prospectiveSkipped += prospective.length;
2963
2982
  counters.placeholderSkipped += placeholder.length;
2983
+ counters.namespacedSkipped += namespaced.length;
2964
2984
  const missing = [];
2965
2985
  for (const claim of paths) {
2966
2986
  if (await env.pathResolves(claim)) examined.push({ kind: "path", value: claim, exists: true });
@@ -2970,6 +2990,11 @@ async function checkTextClaims(env, file, opts, counters) {
2970
2990
  const gitignored = new Set((ignoredOut ?? "").split("\n").filter(Boolean));
2971
2991
  let pathFindings = 0;
2972
2992
  for (const claim of missing) {
2993
+ if (opts.rootedFirstSegments && !opts.rootedFirstSegments.has(claim.split("/")[0] ?? claim)) {
2994
+ counters.outsideRepoSkipped += 1;
2995
+ examined.push({ kind: "path", value: claim, exists: null });
2996
+ continue;
2997
+ }
2973
2998
  if (gitignored.has(claim)) {
2974
2999
  counters.gitignoredSkipped += 1;
2975
3000
  examined.push({ kind: "path", value: claim, exists: null });
@@ -3469,14 +3494,14 @@ function expandTilde(p) {
3469
3494
  if (p.startsWith("~/")) return path2.join(os.homedir(), p.slice(2));
3470
3495
  return p;
3471
3496
  }
3472
- var REBUILD_RECIPE = 'rebuild it beside the manifest: {"machineProfile":"corp","dirs":{"<name>":"~/path/to/worktree"}}';
3497
+ var REBUILD_RECIPE = 'rebuild it beside the manifest: {"machineProfile":"guarded","dirs":{"<name>":"~/path/to/worktree"}}';
3473
3498
  var SAFE_NAME = /^[^\s/\\:.][^\s/\\:]*$/;
3474
3499
  function rejectUnsafeName(name, where, manifest, manifestFile) {
3475
3500
  if (SAFE_NAME.test(name)) return false;
3476
3501
  manifest.problems.push({
3477
3502
  kind: "bad-shape",
3478
3503
  file: manifestFile,
3479
- detail: `${where} name ${JSON.stringify(name)} is not a single path-safe segment \u2014 entry skipped (names build finding ids and corp/<name>/ persistence paths)`
3504
+ detail: `${where} name ${JSON.stringify(name)} is not a single path-safe segment \u2014 entry skipped (names build finding ids and guarded/<name>/ persistence paths)`
3480
3505
  });
3481
3506
  return true;
3482
3507
  }
@@ -3517,7 +3542,7 @@ async function readLocal(localPath, problems) {
3517
3542
  }
3518
3543
  }
3519
3544
  function loadRegistryEntries(projects, manifest, manifestFile) {
3520
- const corpNames = [];
3545
+ const guardedNames = [];
3521
3546
  for (const [i, rawEntry] of projects.entries()) {
3522
3547
  const rec = asRecord(rawEntry);
3523
3548
  const name = rec ? asString(rec.name) : void 0;
@@ -3530,8 +3555,8 @@ function loadRegistryEntries(projects, manifest, manifestFile) {
3530
3555
  continue;
3531
3556
  }
3532
3557
  if (rejectUnsafeName(name, `projects[${i}]`, manifest, manifestFile)) continue;
3533
- const profile = rec.profile === "corp" ? "corp" : "personal";
3534
- if (rec.profile !== void 0 && rec.profile !== "corp" && rec.profile !== "personal") {
3558
+ const profile = rec.profile === "guarded" ? "guarded" : "personal";
3559
+ if (rec.profile !== void 0 && rec.profile !== "guarded" && rec.profile !== "personal") {
3535
3560
  manifest.problems.push({
3536
3561
  kind: "bad-shape",
3537
3562
  file: manifestFile,
@@ -3569,12 +3594,12 @@ function loadRegistryEntries(projects, manifest, manifestFile) {
3569
3594
  contract,
3570
3595
  links: asStringMap(rec.links)
3571
3596
  };
3572
- if (entry.profile === "corp" || entry.private) {
3573
- corpNames.push(name);
3597
+ if (entry.profile === "guarded" || entry.private) {
3598
+ guardedNames.push(name);
3574
3599
  if (!manifest.localPresent) {
3575
3600
  entry.unresolved = `registry.local.json is absent \u2014 ${REBUILD_RECIPE}`;
3576
3601
  } else if (manifest.machineProfile === "personal") {
3577
- entry.unresolved = 'corp entries do not resolve under machineProfile "personal" \u2014 absent on this machine by design';
3602
+ entry.unresolved = 'guarded entries do not resolve under machineProfile "personal" \u2014 absent on this machine by design';
3578
3603
  } else {
3579
3604
  const dir = manifest.localDirs[name];
3580
3605
  if (!dir) {
@@ -3590,11 +3615,11 @@ function loadRegistryEntries(projects, manifest, manifestFile) {
3590
3615
  }
3591
3616
  manifest.entries.push(entry);
3592
3617
  }
3593
- if (corpNames.length && !manifest.localPresent) {
3618
+ if (guardedNames.length && !manifest.localPresent) {
3594
3619
  manifest.problems.push({
3595
3620
  kind: "local-missing",
3596
3621
  file: path2.basename(manifest.localPath),
3597
- detail: `${corpNames.length} corp/private entr${corpNames.length === 1 ? "y" : "ies"} (${corpNames.join(", ")}) cannot resolve without it \u2014 ${REBUILD_RECIPE}`
3622
+ detail: `${guardedNames.length} guarded/private entr${guardedNames.length === 1 ? "y" : "ies"} (${guardedNames.join(", ")}) cannot resolve without it \u2014 ${REBUILD_RECIPE}`
3598
3623
  });
3599
3624
  }
3600
3625
  }
@@ -3643,7 +3668,7 @@ async function loadFleetManifest(manifestPath) {
3643
3668
  shape: "registry",
3644
3669
  manifestPath: abs,
3645
3670
  dir,
3646
- corpHosts: [],
3671
+ guardedHosts: [],
3647
3672
  labels: {},
3648
3673
  localDirs: {},
3649
3674
  localPath: path2.join(dir, "registry.local.json"),
@@ -3676,8 +3701,8 @@ async function loadFleetManifest(manifestPath) {
3676
3701
  manifest.localPresent = local.present;
3677
3702
  manifest.localDirs = asStringMap(local.data.dirs);
3678
3703
  manifest.labels = asStringMap(local.data.labels);
3679
- manifest.corpHosts = Array.isArray(local.data.corpHosts) ? local.data.corpHosts.filter((h) => typeof h === "string") : [];
3680
- manifest.machineProfile = local.data.machineProfile === "corp" || local.data.machineProfile === "personal" ? local.data.machineProfile : void 0;
3704
+ manifest.guardedHosts = Array.isArray(local.data.guardedHosts) ? local.data.guardedHosts.filter((h) => typeof h === "string") : [];
3705
+ manifest.machineProfile = local.data.machineProfile === "guarded" || local.data.machineProfile === "personal" ? local.data.machineProfile : void 0;
3681
3706
  const rootRaw = asString(local.data.root) ?? asString(parsed.root);
3682
3707
  if (rootRaw) manifest.root = path2.resolve(dir, expandTilde(rootRaw));
3683
3708
  const orientationRec = asRecord(parsed.orientation);
@@ -3720,10 +3745,136 @@ async function loadFleetManifest(manifestPath) {
3720
3745
  init_config();
3721
3746
  init_facts();
3722
3747
  init_util();
3748
+
3749
+ // src/engine/milestones.ts
3750
+ var MILESTONE_COLUMNS = [
3751
+ "id",
3752
+ "milestone",
3753
+ "goal",
3754
+ "status",
3755
+ "next",
3756
+ "effort",
3757
+ "depends-on"
3758
+ ];
3759
+ var MILESTONE_GOALS = ["1", "2", "3"];
3760
+ var MILESTONE_STATUSES = ["planned", "active", "blocked", "done"];
3761
+ var MILESTONE_EFFORTS = ["S", "M", "L"];
3762
+ var NONE_MARKERS = /* @__PURE__ */ new Set(["", "\u2014", "-", "\u2013", "none"]);
3763
+ function splitRow(line) {
3764
+ const trimmed = line.trim().replace(/^\|/, "").replace(/\|$/, "");
3765
+ return trimmed.split("|").map((c) => c.trim());
3766
+ }
3767
+ function isSeparatorRow(line) {
3768
+ const cells = splitRow(line);
3769
+ return cells.length > 0 && cells.every((c) => /^:?-{1,}:?$/.test(c));
3770
+ }
3771
+ function firstTable(lines, startLine) {
3772
+ for (let i = startLine; i < lines.length - 1; i++) {
3773
+ const line = lines[i] ?? "";
3774
+ const next = lines[i + 1] ?? "";
3775
+ if (!line.trim().startsWith("|") || !isSeparatorRow(next)) continue;
3776
+ const header = splitRow(line).map((h) => h.toLowerCase());
3777
+ const rows = [];
3778
+ for (let j = i + 2; j < lines.length; j++) {
3779
+ const body = lines[j] ?? "";
3780
+ if (!body.trim().startsWith("|")) break;
3781
+ rows.push({ line: j + 1, cells: splitRow(body) });
3782
+ }
3783
+ return { header, rows, headerLine: i + 1 };
3784
+ }
3785
+ return null;
3786
+ }
3787
+ function parseList(cell) {
3788
+ if (NONE_MARKERS.has(cell.trim().toLowerCase())) return [];
3789
+ return cell.split(",").map((s) => s.trim()).filter(Boolean);
3790
+ }
3791
+ function checkColumns(table, expected, problems) {
3792
+ const got = table.header.join(" | ");
3793
+ const want = expected.join(" | ");
3794
+ if (got !== want) {
3795
+ problems.push(
3796
+ `line ${table.headerLine}: table columns are \`${got}\`; the standard is \`${want}\``
3797
+ );
3798
+ return false;
3799
+ }
3800
+ return true;
3801
+ }
3802
+ function checkVocabulary(line, field, value, allowed, problems) {
3803
+ if (allowed.includes(value)) return true;
3804
+ problems.push(
3805
+ `line ${line}: ${field} is \`${value}\`; allowed: ${allowed.map((a) => `\`${a}\``).join(", ")}`
3806
+ );
3807
+ return false;
3808
+ }
3809
+ function parseMilestones(text) {
3810
+ const problems = [];
3811
+ const lines = text.split(/\r?\n/);
3812
+ const headingIdx = lines.findIndex((l) => /^#\s/.test(l));
3813
+ if (headingIdx === -1 || lines[headingIdx]?.trim() !== "# Milestones") {
3814
+ problems.push(
3815
+ headingIdx === -1 ? "no heading: the file must start with `# Milestones`" : `line ${headingIdx + 1}: first heading is \`${lines[headingIdx]?.trim()}\`; it must be \`# Milestones\``
3816
+ );
3817
+ }
3818
+ const table = firstTable(lines, headingIdx === -1 ? 0 : headingIdx + 1);
3819
+ if (!table) {
3820
+ problems.push(
3821
+ `no milestones table: expected \`| ${MILESTONE_COLUMNS.join(" | ")} |\` after the heading`
3822
+ );
3823
+ return { rows: [], problems };
3824
+ }
3825
+ if (!checkColumns(table, MILESTONE_COLUMNS, problems)) return { rows: [], problems };
3826
+ const rows = [];
3827
+ const ids = /* @__PURE__ */ new Set();
3828
+ for (const { line, cells } of table.rows) {
3829
+ if (cells.length !== MILESTONE_COLUMNS.length) {
3830
+ problems.push(`line ${line}: ${cells.length} cells, expected ${MILESTONE_COLUMNS.length}`);
3831
+ continue;
3832
+ }
3833
+ const [id, milestone, goal, status, next, effort, deps] = cells;
3834
+ let ok = true;
3835
+ if (!/^M\d+$/.test(id)) {
3836
+ problems.push(`line ${line}: id \`${id}\` is not \`M<number>\``);
3837
+ ok = false;
3838
+ } else if (ids.has(id)) {
3839
+ problems.push(`line ${line}: id \`${id}\` is used twice`);
3840
+ ok = false;
3841
+ }
3842
+ if (!milestone) {
3843
+ problems.push(`line ${line}: milestone title is empty`);
3844
+ ok = false;
3845
+ }
3846
+ if (!next) {
3847
+ problems.push(`line ${line}: next step is empty \u2014 a milestone without a next step is a wish`);
3848
+ ok = false;
3849
+ }
3850
+ ok = checkVocabulary(line, "goal", goal, MILESTONE_GOALS, problems) && ok;
3851
+ ok = checkVocabulary(line, "status", status, MILESTONE_STATUSES, problems) && ok;
3852
+ ok = checkVocabulary(line, "effort", effort, MILESTONE_EFFORTS, problems) && ok;
3853
+ if (!ok) continue;
3854
+ ids.add(id);
3855
+ rows.push({
3856
+ id,
3857
+ milestone,
3858
+ goal,
3859
+ status,
3860
+ next,
3861
+ effort,
3862
+ dependsOn: parseList(deps)
3863
+ });
3864
+ }
3865
+ for (const r of rows) {
3866
+ for (const d of r.dependsOn) {
3867
+ if (!ids.has(d)) problems.push(`${r.id} depends on \`${d}\`, which is not in the table`);
3868
+ }
3869
+ }
3870
+ return { rows, problems };
3871
+ }
3872
+
3873
+ // src/engine/fleet.ts
3723
3874
  var pExecFile3 = promisify(execFile);
3724
3875
  var FLEET_LENS = "fleet-manifest";
3725
3876
  var FLEET_JSON_SCHEMA = "fleet-experimental-0.2";
3726
- var CORP_WALL_ARTIFACTS = ["PROJECT_CONTEXT.md", "DECISIONS.md"];
3877
+ var GUARDED_WALL_ARTIFACTS = ["PROJECT_CONTEXT.md", "DECISIONS.md"];
3727
3878
  function recurringClasses(projects) {
3728
3879
  const byClass = /* @__PURE__ */ new Map();
3729
3880
  for (const p of projects) {
@@ -3756,12 +3907,12 @@ function finding3(id, tier, claim, evidence, why, action) {
3756
3907
  confidence: "high"
3757
3908
  };
3758
3909
  }
3759
- function corpPersistenceRoot(manifest, name) {
3760
- const corpDir = path2.join(manifest.dir, "corp");
3761
- const root = path2.resolve(corpDir, name);
3762
- if (path2.dirname(root) !== corpDir || root === corpDir) {
3910
+ function guardedPersistenceRoot(manifest, name) {
3911
+ const guardedDir = path2.join(manifest.dir, "guarded");
3912
+ const root = path2.resolve(guardedDir, name);
3913
+ if (path2.dirname(root) !== guardedDir || root === guardedDir) {
3763
3914
  throw new Error(
3764
- `entry name ${JSON.stringify(name)} escapes the corp persistence zone \u2014 refusing to resolve a ledger root for it`
3915
+ `entry name ${JSON.stringify(name)} escapes the guarded persistence zone \u2014 refusing to resolve a ledger root for it`
3765
3916
  );
3766
3917
  }
3767
3918
  return root;
@@ -3794,7 +3945,7 @@ async function realpathOr(p) {
3794
3945
  return path2.resolve(p);
3795
3946
  }
3796
3947
  }
3797
- function emailMatchesCorpHosts(email, hosts) {
3948
+ function emailMatchesGuardedHosts(email, hosts) {
3798
3949
  const domain = email.split("@")[1]?.toLowerCase();
3799
3950
  if (!domain) return false;
3800
3951
  return hosts.some((h) => {
@@ -3845,7 +3996,7 @@ async function checkManifest(manifest) {
3845
3996
  );
3846
3997
  }
3847
3998
  seen.add(entry.name);
3848
- if (entry.profile !== "corp" && !entry.private && !entry.trust) {
3999
+ if (entry.profile !== "guarded" && !entry.private && !entry.trust) {
3849
4000
  const bad = entry.trustRaw;
3850
4001
  findings.push(
3851
4002
  finding3(
@@ -3883,14 +4034,14 @@ async function checkManifest(manifest) {
3883
4034
  )
3884
4035
  );
3885
4036
  }
3886
- if (entry.unresolved && manifest.localPresent && manifest.machineProfile !== "personal" && (entry.profile === "corp" || entry.private)) {
4037
+ if (entry.unresolved && manifest.localPresent && manifest.machineProfile !== "personal" && (entry.profile === "guarded" || entry.private)) {
3887
4038
  findings.push(
3888
4039
  finding3(
3889
4040
  `${FLEET_LENS}/dangling-dir:${entry.name}`,
3890
4041
  "gap",
3891
4042
  `\`${entry.name}\` has no usable directory mapping`,
3892
4043
  [`${localFile}: ${entry.unresolved}`],
3893
- "An unmapped corp entry silently drops out of every sweep on the machine that most needs it.",
4044
+ "An unmapped guarded entry silently drops out of every sweep on the machine that most needs it.",
3894
4045
  `Add a \`dirs\` mapping for \`${entry.name}\` in ${localFile}.`
3895
4046
  )
3896
4047
  );
@@ -3954,28 +4105,28 @@ async function checkManifest(manifest) {
3954
4105
  );
3955
4106
  }
3956
4107
  if (manifest.machineProfile === "personal") {
3957
- const corpCount = manifest.entries.filter((e) => e.profile === "corp" || e.private).length;
3958
- if (corpCount) {
4108
+ const guardedCount = manifest.entries.filter((e) => e.profile === "guarded" || e.private).length;
4109
+ if (guardedCount) {
3959
4110
  disclosures.push(
3960
- `machineProfile "personal": ${corpCount} corp entr${corpCount === 1 ? "y" : "ies"} deliberately absent on this machine \u2014 disclosed, not dangling.`
4111
+ `machineProfile "personal": ${guardedCount} guarded entr${guardedCount === 1 ? "y" : "ies"} deliberately absent on this machine \u2014 disclosed, not dangling.`
3961
4112
  );
3962
4113
  }
3963
4114
  }
3964
4115
  return { findings, disclosures };
3965
4116
  }
3966
- async function checkCorpWallArtifacts(entries, findings) {
4117
+ async function checkGuardedWallArtifacts(entries, findings) {
3967
4118
  for (const entry of entries) {
3968
- if (entry.profile !== "corp" || !entry.resolvedRoot) continue;
3969
- for (const artifact of CORP_WALL_ARTIFACTS) {
4119
+ if (entry.profile !== "guarded" || !entry.resolvedRoot) continue;
4120
+ for (const artifact of GUARDED_WALL_ARTIFACTS) {
3970
4121
  if (await pathExists(path2.join(entry.resolvedRoot, artifact))) {
3971
4122
  findings.push(
3972
4123
  finding3(
3973
- `${FLEET_LENS}/corp-artifact-in-repo:${entry.name}:${artifact}`,
4124
+ `${FLEET_LENS}/guarded-artifact-in-repo:${entry.name}:${artifact}`,
3974
4125
  "risk",
3975
- `corp entry \`${entry.name}\` carries ${artifact} inside its worktree`,
4126
+ `guarded entry \`${entry.name}\` carries ${artifact} inside its worktree`,
3976
4127
  [`${entry.resolvedRoot}/${artifact}`],
3977
- "Agent artifacts for corp repos live outside the repo by policy \u2014 an in-repo copy is one push away from colleagues' eyes and will diverge from the manifest-side truth.",
3978
- `Move it to the fleet's corp/${entry.name}/ zone and leave a pointer if the harness needs one.`
4128
+ "Agent artifacts for guarded repos live outside the repo by policy \u2014 an in-repo copy is one push away from colleagues' eyes and will diverge from the manifest-side truth.",
4129
+ `Move it to the fleet's guarded/${entry.name}/ zone and leave a pointer if the harness needs one.`
3979
4130
  )
3980
4131
  );
3981
4132
  }
@@ -3983,9 +4134,9 @@ async function checkCorpWallArtifacts(entries, findings) {
3983
4134
  }
3984
4135
  }
3985
4136
  async function checkCoverage(manifest, findings, disclosures) {
3986
- if (!manifest.corpHosts.length) {
4137
+ if (!manifest.guardedHosts.length) {
3987
4138
  disclosures.push(
3988
- "Coverage check skipped \u2014 no corpHosts in the local manifest, so a corp remote cannot be recognized."
4139
+ "Coverage check skipped \u2014 no guardedHosts in the local manifest, so a guarded remote cannot be recognized."
3989
4140
  );
3990
4141
  return;
3991
4142
  }
@@ -4006,16 +4157,16 @@ async function checkCoverage(manifest, findings, disclosures) {
4006
4157
  if (!await pathExists(path2.join(dir, ".git"))) continue;
4007
4158
  const remotes = await git(dir, ["config", "--get-regexp", "^remote\\..*\\.url"]);
4008
4159
  if (!remotes) continue;
4009
- const corpRemote = manifest.corpHosts.find((h) => remotes.includes(h));
4010
- if (corpRemote) {
4160
+ const guardedRemote = manifest.guardedHosts.find((h) => remotes.includes(h));
4161
+ if (guardedRemote) {
4011
4162
  findings.push(
4012
4163
  finding3(
4013
- `${FLEET_LENS}/unregistered-corp-remote:${d.name}`,
4164
+ `${FLEET_LENS}/unregistered-guarded-remote:${d.name}`,
4014
4165
  "risk",
4015
- `\`${d.name}\` under the fleet root has a corp remote but is not registered`,
4016
- [`${dir}: remote matches ${corpRemote}`],
4017
- "The wall is only as good as its census \u2014 an unregistered employer checkout is corp material no sweep, wall check, or transfer plan knows exists.",
4018
- "Register it as a corp entry (opaque alias + local dirs mapping) or move it out of the fleet root."
4166
+ `\`${d.name}\` under the fleet root has a guarded remote but is not registered`,
4167
+ [`${dir}: remote matches ${guardedRemote}`],
4168
+ "The wall is only as good as its census \u2014 an unregistered guarded checkout is guarded material no sweep, wall check, or transfer plan knows exists.",
4169
+ "Register it as a guarded entry (opaque alias + local dirs mapping) or move it out of the fleet root."
4019
4170
  )
4020
4171
  );
4021
4172
  }
@@ -4063,7 +4214,7 @@ async function checkHygieneNeedles(manifest, findings, disclosures) {
4063
4214
  [
4064
4215
  ...Object.values(manifest.labels),
4065
4216
  ...Object.values(manifest.localDirs).map((d) => path2.basename(expandNeedleDir(d))),
4066
- ...manifest.corpHosts
4217
+ ...manifest.guardedHosts
4067
4218
  ].filter((n) => n.length >= 3)
4068
4219
  )
4069
4220
  ];
@@ -4090,7 +4241,7 @@ async function checkHygieneNeedles(manifest, findings, disclosures) {
4090
4241
  "risk",
4091
4242
  `${file} in public-repo \`${entry.name}\` carries a needle from the fleet's private local manifest`,
4092
4243
  [`${file}: contains "${needle}"`],
4093
- "A public repo that names a private label, corp directory, or corp host joins the public and employer identities for anyone who greps.",
4244
+ "A public repo that names a private label, guarded directory, or guarded host joins the public and guarded identities for anyone who greps.",
4094
4245
  "Scrub the needle from the tracked file (rewrite history if it already shipped)."
4095
4246
  )
4096
4247
  );
@@ -4101,10 +4252,10 @@ async function checkHygieneNeedles(manifest, findings, disclosures) {
4101
4252
  function expandNeedleDir(dir) {
4102
4253
  return dir.replace(/\/+$/, "");
4103
4254
  }
4104
- async function checkCorpEmails(manifest, findings, disclosures) {
4105
- if (!manifest.corpHosts.length) {
4255
+ async function checkGuardedEmails(manifest, findings, disclosures) {
4256
+ if (!manifest.guardedHosts.length) {
4106
4257
  disclosures.push(
4107
- "Corp-email check skipped \u2014 no corpHosts in the local manifest, so a corp domain cannot be recognized."
4258
+ "Guarded-email check skipped \u2014 no guardedHosts in the local manifest, so a guarded domain cannot be recognized."
4108
4259
  );
4109
4260
  return;
4110
4261
  }
@@ -4113,21 +4264,23 @@ async function checkCorpEmails(manifest, findings, disclosures) {
4113
4264
  const emails = await git(entry.resolvedRoot, ["log", "-30", "--format=%ae"]);
4114
4265
  if (emails === null) {
4115
4266
  disclosures.push(
4116
- `Corp-email check could not run in \`${entry.name}\` (no readable commit history) \u2014 undetermined, not clean.`
4267
+ `Guarded-email check could not run in \`${entry.name}\` (no readable commit history) \u2014 undetermined, not clean.`
4117
4268
  );
4118
4269
  continue;
4119
4270
  }
4120
4271
  const lines = emails.split("\n").filter(Boolean);
4121
- const matched = [...new Set(lines.filter((e) => emailMatchesCorpHosts(e, manifest.corpHosts)))];
4272
+ const matched = [
4273
+ ...new Set(lines.filter((e) => emailMatchesGuardedHosts(e, manifest.guardedHosts)))
4274
+ ];
4122
4275
  if (matched.length) {
4123
4276
  const count = lines.filter((e) => matched.includes(e)).length;
4124
4277
  findings.push(
4125
4278
  finding3(
4126
- `${FLEET_LENS}/corp-email:${entry.name}`,
4279
+ `${FLEET_LENS}/guarded-email:${entry.name}`,
4127
4280
  "risk",
4128
- `recent commits on personal entry \`${entry.name}\` are authored with a corp-host email`,
4281
+ `recent commits on personal entry \`${entry.name}\` are authored with a guarded-host email`,
4129
4282
  [`${count} of the last ${lines.length} commits: ${matched.join(", ")}`],
4130
- "A corp email in a personal repo's history ties the two identities together permanently \u2014 and silently, since the machine's global git identity is usually the corp one.",
4283
+ "A guarded email in a personal repo's history ties the two identities together permanently \u2014 and silently, since the machine's global git identity is usually the guarded one.",
4131
4284
  "Set the repo-local identity to the personal one and amend the unpushed commits."
4132
4285
  )
4133
4286
  );
@@ -4141,7 +4294,7 @@ async function checkGateDrift(manifest, findings, disclosures) {
4141
4294
  for (const entry of manifest.entries) {
4142
4295
  const root = entry.resolvedRoot;
4143
4296
  if (!root || !await isDirectory(root)) continue;
4144
- if (entry.profile === "corp" || entry.private) continue;
4297
+ if (entry.profile === "guarded" || entry.private) continue;
4145
4298
  if (entry.gates === "none") {
4146
4299
  disclosures.push(`${entry.name}: gates declared \`none\` \u2014 deliberately absent, not missing`);
4147
4300
  continue;
@@ -4194,11 +4347,11 @@ async function checkGateDrift(manifest, findings, disclosures) {
4194
4347
  async function collectWallFindings(manifest) {
4195
4348
  const findings = [];
4196
4349
  const disclosures = [];
4197
- await checkCorpWallArtifacts(manifest.entries, findings);
4350
+ await checkGuardedWallArtifacts(manifest.entries, findings);
4198
4351
  await checkCoverage(manifest, findings, disclosures);
4199
4352
  await checkManifestRepoMachinePaths(manifest, findings, disclosures);
4200
4353
  await checkHygieneNeedles(manifest, findings, disclosures);
4201
- await checkCorpEmails(manifest, findings, disclosures);
4354
+ await checkGuardedEmails(manifest, findings, disclosures);
4202
4355
  await checkGateDrift(manifest, findings, disclosures);
4203
4356
  return { findings, disclosures };
4204
4357
  }
@@ -4264,13 +4417,13 @@ async function sweepEntry(manifest, entry, opts) {
4264
4417
  );
4265
4418
  }
4266
4419
  }
4267
- const corp = entry.profile === "corp";
4268
- const optedIn = !corp && await pathExists(path2.join(root, ETYMD_DIR));
4420
+ const guarded = entry.profile === "guarded";
4421
+ const optedIn = !guarded && await pathExists(path2.join(root, ETYMD_DIR));
4269
4422
  const result = await runAudit(root, {
4270
4423
  kind: opts.kind,
4271
- persistLedger: !corp && opts.persistLedgers === true && optedIn,
4272
- readOnlyRoot: corp,
4273
- ledgerRoot: corp ? corpPersistenceRoot(manifest, entry.name) : void 0,
4424
+ persistLedger: !guarded && opts.persistLedgers === true && optedIn,
4425
+ readOnlyRoot: guarded,
4426
+ ledgerRoot: guarded ? guardedPersistenceRoot(manifest, entry.name) : void 0,
4274
4427
  stateBudgets: stateBudgetsFor(entry),
4275
4428
  upstreamRemote
4276
4429
  });
@@ -4278,11 +4431,43 @@ async function sweepEntry(manifest, entry, opts) {
4278
4431
  if (entry.contract.placement === "none") {
4279
4432
  project.findings = project.findings.filter((f) => !f.id.endsWith("/no-contract"));
4280
4433
  }
4434
+ const milestonesFile = entry.contract.milestones;
4435
+ if (milestonesFile && milestonesFile !== "none") {
4436
+ const text = await readText(path2.join(root, milestonesFile));
4437
+ if (text === null) {
4438
+ project.findings.push(
4439
+ finding3(
4440
+ `${FLEET_LENS}/milestones-missing:${entry.name}`,
4441
+ "gap",
4442
+ `\`${entry.name}\` declares milestones at \`${milestonesFile}\` and the file is absent`,
4443
+ [
4444
+ `${path2.basename(manifest.manifestPath)}: ${entry.name}.contract.milestones = ${milestonesFile}`
4445
+ ],
4446
+ "The fleet board rolls every project's plan up from this file; a declared file that is absent is a row the board cannot fill.",
4447
+ 'Create it (heading `# Milestones`, then the standard table), or declare `milestones: "none"` if this project deliberately carries no plan.'
4448
+ )
4449
+ );
4450
+ } else {
4451
+ const doc = parseMilestones(text);
4452
+ doc.problems.forEach((problem, i) => {
4453
+ project.findings.push(
4454
+ finding3(
4455
+ `${FLEET_LENS}/milestones-shape:${entry.name}:${i + 1}`,
4456
+ "gap",
4457
+ `\`${entry.name}\` ${milestonesFile}: ${problem}`,
4458
+ [`${milestonesFile}`],
4459
+ "A milestones file the board cannot parse is a plan the fleet cannot see.",
4460
+ `Fix the table to the standard shape: \`| ${MILESTONE_COLUMNS.join(" | ")} |\`.`
4461
+ )
4462
+ );
4463
+ });
4464
+ }
4465
+ }
4281
4466
  for (const f of project.findings) project.counts[f.tier] += 1;
4282
4467
  project.stateAgeDays = stateAgeDays(result.facts);
4283
4468
  project.staleAfterDays = result.config.config.state.staleAfterDays;
4284
4469
  for (const [key, value] of Object.entries(entry.contract)) {
4285
- if (key === "placement" || !value) continue;
4470
+ if (key === "placement" || key === "milestones" || !value) continue;
4286
4471
  const exists = await pathExists(path2.join(root, value.replace(/#.*$/, "")));
4287
4472
  project.disclosures.push(
4288
4473
  `contract override \`${key}: ${value}\` \u2014 registered, existence-checked only (${exists ? "present" : "MISSING"})`
@@ -4325,6 +4510,179 @@ async function sweepFleet(manifest, opts = {}) {
4325
4510
  };
4326
4511
  }
4327
4512
 
4513
+ // src/engine/propose.ts
4514
+ var PROPOSAL_SCHEMA = "proposal/1";
4515
+ var RUBRIC_CRITERIA = ["severity", "economy", "confidence", "breadth"];
4516
+ function parseRubric(text, file) {
4517
+ const lines = [];
4518
+ const seen = /* @__PURE__ */ new Map();
4519
+ const rows = text.split(/\r?\n/);
4520
+ for (const [i, raw] of rows.entries()) {
4521
+ const line = raw.trim();
4522
+ if (!line || line.startsWith("#")) continue;
4523
+ const at = i + 1;
4524
+ const refuse = (why) => new Error(
4525
+ `${file}:${at} \`${raw.trim()}\` \u2014 ${why} (criteria: ${RUBRIC_CRITERIA.join(", ")})`
4526
+ );
4527
+ const sep = line.indexOf(":");
4528
+ if (sep < 0) throw refuse("expected `criterion: <weight>`");
4529
+ const criterion = line.slice(0, sep).trim();
4530
+ const weightRaw = line.slice(sep + 1).trim();
4531
+ if (!RUBRIC_CRITERIA.includes(criterion)) {
4532
+ throw refuse(`unknown criterion \`${criterion}\``);
4533
+ }
4534
+ if (!/^\d+$/.test(weightRaw) || Number(weightRaw) < 1) {
4535
+ throw refuse(`weight \`${weightRaw}\` is not a positive integer`);
4536
+ }
4537
+ const known = criterion;
4538
+ if (seen.has(known)) {
4539
+ throw new Error(
4540
+ `${file}:${at} \`${raw.trim()}\` \u2014 criterion \`${criterion}\` is already weighted on line ${seen.get(known)}; one line per criterion`
4541
+ );
4542
+ }
4543
+ seen.set(known, at);
4544
+ lines.push({ criterion: known, weight: Number(weightRaw) });
4545
+ }
4546
+ if (!lines.length) {
4547
+ throw new Error(`${file} carries no criterion lines \u2014 a rubric with no criteria scores nothing`);
4548
+ }
4549
+ return { lines };
4550
+ }
4551
+ var TIER_VALUE = { risk: 3, gap: 2, polish: 1 };
4552
+ var EFFORT_VALUE = { S: 3, M: 2, L: 1 };
4553
+ var CONFIDENCE_VALUE = { high: 3, medium: 2, low: 1 };
4554
+ function findingSubject(project, f) {
4555
+ return {
4556
+ id: `${project.name}:${f.id}`,
4557
+ kind: "finding",
4558
+ classId: f.id.split(":")[0] ?? f.id,
4559
+ tier: f.tier,
4560
+ effort: f.effort,
4561
+ confidence: f.confidence,
4562
+ projects: [project.name],
4563
+ action: f.action ?? "",
4564
+ evidence: f.evidence
4565
+ };
4566
+ }
4567
+ function classSubject(classId, members) {
4568
+ const pick = (order, at, dir) => {
4569
+ const values = members.map((m) => order[at(m.finding)]);
4570
+ const value = dir === 1 ? Math.max(...values) : Math.min(...values);
4571
+ return Object.entries(order).find(([, v]) => v === value)[0];
4572
+ };
4573
+ const actions = [...new Set(members.map((m) => m.finding.action).filter(Boolean))].sort();
4574
+ return {
4575
+ id: `class:${classId}`,
4576
+ kind: "class",
4577
+ classId,
4578
+ tier: pick(TIER_VALUE, (f) => f.tier, 1),
4579
+ effort: pick(EFFORT_VALUE, (f) => f.effort, -1),
4580
+ confidence: pick(CONFIDENCE_VALUE, (f) => f.confidence, -1),
4581
+ projects: [...new Set(members.map((m) => m.project.name))].sort(),
4582
+ action: actions.join("; "),
4583
+ evidence: members.flatMap((m) => m.finding.evidence)
4584
+ };
4585
+ }
4586
+ var CRITERION_VALUE = {
4587
+ severity: (s) => TIER_VALUE[s.tier],
4588
+ economy: (s) => EFFORT_VALUE[s.effort],
4589
+ confidence: (s) => CONFIDENCE_VALUE[s.confidence],
4590
+ breadth: (s) => Math.min(3, s.projects.length)
4591
+ };
4592
+ var FILENAME_RE = /^[^\s:]+\.[A-Za-z0-9]+$/;
4593
+ function extractFiles(evidence) {
4594
+ const files = /* @__PURE__ */ new Set();
4595
+ for (const line of evidence) {
4596
+ const token = line.split(/[\s:]/)[0];
4597
+ if (token && (token.includes("/") || FILENAME_RE.test(token))) files.add(token);
4598
+ }
4599
+ return [...files].sort();
4600
+ }
4601
+ function extractGates(files) {
4602
+ return files.filter((f) => f.startsWith(".githooks/") || f.startsWith(".github/workflows/"));
4603
+ }
4604
+ function reversibility(files) {
4605
+ if (!files.length) return "undetermined";
4606
+ const generated = (f) => f.startsWith(".etymd/") || f.startsWith(".githooks/");
4607
+ return files.every(generated) ? "regenerable" : "git-reversible";
4608
+ }
4609
+ function toProposal(s, rubric) {
4610
+ const matched = [];
4611
+ let score = 0;
4612
+ for (const line of rubric.lines) {
4613
+ const value = CRITERION_VALUE[line.criterion](s);
4614
+ score += line.weight * value;
4615
+ if (value >= 2) matched.push({ criterion: line.criterion, weight: line.weight, value });
4616
+ }
4617
+ const files = extractFiles(s.evidence);
4618
+ return {
4619
+ id: s.id,
4620
+ schema: PROPOSAL_SCHEMA,
4621
+ class: s.classId,
4622
+ kind: s.kind,
4623
+ projects: s.projects,
4624
+ action: s.action,
4625
+ effort: s.effort,
4626
+ confidence: s.confidence,
4627
+ score,
4628
+ matched,
4629
+ implications: {
4630
+ projects: s.projects,
4631
+ files,
4632
+ gates: extractGates(files),
4633
+ reversibility: reversibility(files)
4634
+ }
4635
+ };
4636
+ }
4637
+ function buildProposals(sweep, rubricName, rubric) {
4638
+ const disclosures = [];
4639
+ const personal = sweep.projects.filter((p) => p.profile === "personal");
4640
+ const guarded = sweep.projects.filter((p) => p.profile !== "personal");
4641
+ if (guarded.length) {
4642
+ disclosures.push(
4643
+ `guarded entries excluded from proposals: ${guarded.map((p) => p.name).sort().join(", ")}`
4644
+ );
4645
+ }
4646
+ const notAudited = personal.filter((p) => !p.resolvedRoot || p.unresolved);
4647
+ if (notAudited.length) {
4648
+ disclosures.push(
4649
+ `not audited, so not proposed: ${notAudited.map((p) => p.name).sort().join(", ")}`
4650
+ );
4651
+ }
4652
+ const subjects = [];
4653
+ for (const project of personal) {
4654
+ for (const f of project.findings) {
4655
+ if (f.kind === "improvement") subjects.push(findingSubject(project, f));
4656
+ }
4657
+ }
4658
+ const byClass = /* @__PURE__ */ new Map();
4659
+ for (const project of personal) {
4660
+ for (const f of project.findings) {
4661
+ const classId = f.id.split(":")[0] ?? f.id;
4662
+ const entry = byClass.get(classId) ?? [];
4663
+ entry.push({ project, finding: f });
4664
+ byClass.set(classId, entry);
4665
+ }
4666
+ }
4667
+ for (const [classId, members] of byClass) {
4668
+ if (new Set(members.map((m) => m.project.name)).size >= 2) {
4669
+ subjects.push(classSubject(classId, members));
4670
+ }
4671
+ }
4672
+ const proposals = subjects.map((s) => toProposal(s, rubric)).sort((a, b) => b.score - a.score || a.id.localeCompare(b.id));
4673
+ return {
4674
+ schema: PROPOSAL_SCHEMA,
4675
+ manifest: sweep.manifest,
4676
+ rubric: rubricName,
4677
+ criteria: rubric.lines,
4678
+ proposals,
4679
+ disclosures: [
4680
+ ...disclosures,
4681
+ "implications.files are path-shaped tokens extracted from finding evidence \u2014 evidence lines carrying no path are not represented there"
4682
+ ]
4683
+ };
4684
+ }
4685
+
4328
4686
  // src/engine/premise.ts
4329
4687
  init_facts();
4330
4688
  init_scan();
@@ -4347,7 +4705,8 @@ function promoteBareTokens(text, ctx) {
4347
4705
  bareInvocations: 0,
4348
4706
  proseScripts: 0,
4349
4707
  hostnameLike: 0,
4350
- unrootedDirs: 0
4708
+ unrootedDirs: 0,
4709
+ namespaced: 0
4351
4710
  };
4352
4711
  const promoted = text.split(CODE_SPAN_RE).map((segment, i) => i % 2 === 1 ? segment : promoteProse(segment, ctx, skips)).join("");
4353
4712
  return { text: promoted, skips };
@@ -4366,28 +4725,44 @@ function promoteProse(segment, ctx, skips) {
4366
4725
  }
4367
4726
  return `\`${core}\`${trail}`;
4368
4727
  });
4369
- return withCommands.replace(/[^\s`]+/g, (token) => {
4370
- const m = WRAP_RE.exec(token);
4371
- if (!m) return token;
4372
- const [, lead = "", core = "", trail = ""] = m;
4373
- if (!core || core.includes("://") || !PATH_TOKEN_RE.test(core)) return token;
4374
- const first = core.slice(0, core.indexOf("/"));
4375
- const hostSuffix = HOSTNAME_RE.exec(first)?.[1]?.toLowerCase();
4376
- if (hostSuffix && !KNOWN_EXTENSIONS.has(hostSuffix)) {
4377
- skips.hostnameLike += 1;
4728
+ const parts = withCommands.split(/(\s+)/);
4729
+ for (let i = 0; i < parts.length; i += 2) {
4730
+ parts[i] = promoteToken(parts[i], i >= 2 ? parts[i - 2] : "", ctx, skips);
4731
+ }
4732
+ return parts.join("");
4733
+ }
4734
+ function namespaceOf(core, prev) {
4735
+ const ns = new RegExp(`^(${NAMESPACE_IDENT}):`).exec(core)?.[1] ?? new RegExp(`^(${NAMESPACE_IDENT}):$`).exec(prev)?.[1];
4736
+ return ns && !NAMESPACE_STOP.has(ns.toLowerCase()) ? ns : null;
4737
+ }
4738
+ function promoteToken(token, prev, ctx, skips) {
4739
+ const m = WRAP_RE.exec(token);
4740
+ if (!m) return token;
4741
+ const [, lead = "", core = "", trail = ""] = m;
4742
+ if (!core || core.includes("://")) return token;
4743
+ const namespace = namespaceOf(core, prev);
4744
+ const body = namespace && core.startsWith(`${namespace}:`) ? core.slice(namespace.length + 1) : core;
4745
+ if (!PATH_TOKEN_RE.test(body)) return token;
4746
+ const first = body.slice(0, body.indexOf("/"));
4747
+ const hostSuffix = HOSTNAME_RE.exec(first)?.[1]?.toLowerCase();
4748
+ if (hostSuffix && !KNOWN_EXTENSIONS.has(hostSuffix)) {
4749
+ skips.hostnameLike += 1;
4750
+ return token;
4751
+ }
4752
+ if (body.endsWith("/")) {
4753
+ if (!ctx.rootedDirs.has(first)) {
4754
+ skips.unrootedDirs += 1;
4378
4755
  return token;
4379
4756
  }
4380
- if (core.endsWith("/")) {
4381
- if (!ctx.rootedDirs.has(first)) {
4382
- skips.unrootedDirs += 1;
4383
- return token;
4384
- }
4385
- return `${lead}\`${core}\`${trail}`;
4386
- }
4387
- const ext = core.toLowerCase().match(/\.([a-z0-9]{1,8})$/)?.[1];
4757
+ } else {
4758
+ const ext = body.toLowerCase().match(/\.([a-z0-9]{1,8})$/)?.[1];
4388
4759
  if (!ext || !KNOWN_EXTENSIONS.has(ext)) return token;
4389
- return `${lead}\`${core}\`${trail}`;
4390
- });
4760
+ }
4761
+ if (namespace) {
4762
+ skips.namespaced += 1;
4763
+ return token;
4764
+ }
4765
+ return `${lead}\`${body}\`${trail}`;
4391
4766
  }
4392
4767
  async function listRootedDirs(env) {
4393
4768
  const dirs = /* @__PURE__ */ new Set();
@@ -4405,7 +4780,8 @@ async function runPremise(opts) {
4405
4780
  const facts = await scanProject(root);
4406
4781
  const env = await buildTruthEnv(root, facts);
4407
4782
  const counters = emptyCounters();
4408
- const promoted = promoteBareTokens(task, { rootedDirs: await listRootedDirs(env) });
4783
+ const rootedDirs = await listRootedDirs(env);
4784
+ const promoted = promoteBareTokens(task, { rootedDirs });
4409
4785
  const text = { path: TASK_LABEL, text: promoted.text };
4410
4786
  const findings = [];
4411
4787
  const disclosures = [];
@@ -4418,6 +4794,8 @@ async function runPremise(opts) {
4418
4794
  subject: "The task",
4419
4795
  missingPathTier: "risk",
4420
4796
  maxPathFindings: MAX_PATH_FINDINGS,
4797
+ rootedFirstSegments: rootedDirs,
4798
+ treatNamespacedPrefixes: true,
4421
4799
  whyCommand: "The task is built on a command that does not exist \u2014 an agent will run it and fail, or quietly substitute something else and report success.",
4422
4800
  actionCommand: "Fix the task before handing it over: name the real script, or restore it.",
4423
4801
  whyPath: "The task is ABOUT this path. Acting on it means editing, testing, or reasoning about something that is not there \u2014 the wrong problem, solved precisely.",
@@ -4474,6 +4852,21 @@ async function runPremise(opts) {
4474
4852
  `${skips.unrootedDirs} slash-joined phrase(s) ending in \`/\` start with no directory that exists here (e.g. \`input/output/\`) \u2014 read as prose; skipped, not flagged. Backtick one to have it checked.`
4475
4853
  );
4476
4854
  }
4855
+ if (skips.namespaced) {
4856
+ disclosures.push(
4857
+ `${skips.namespaced} path mention(s) in prose carry a namespace prefix (\`pc:src/x.ts\`, \`lk: src/x.ts\`) \u2014 another repo's tree, never this one; skipped, not flagged.`
4858
+ );
4859
+ }
4860
+ if (counters.namespacedSkipped) {
4861
+ disclosures.push(
4862
+ `${counters.namespacedSkipped} backticked path mention(s) sit directly after a namespace prefix (\`pc: \`src/x.ts\`\`) \u2014 another repo's tree, not this one; skipped, not flagged.`
4863
+ );
4864
+ }
4865
+ if (counters.outsideRepoSkipped) {
4866
+ disclosures.push(
4867
+ `${counters.outsideRepoSkipped} path(s) the task names start at no directory of this repo \u2014 typical of a path quoted from another repository or a scratchpad clone; outside this repo, not missing here. Skipped, not flagged.`
4868
+ );
4869
+ }
4477
4870
  if (counters.unverifiableCommands) {
4478
4871
  disclosures.push(
4479
4872
  `node_modules is not installed \u2014 ${counters.unverifiableCommands} command(s) matching no package script could not be checked against installed binaries; skipped, not flagged.`
@@ -4594,4 +4987,4 @@ wrong problem.
4594
4987
  init_version();
4595
4988
  init_version2();
4596
4989
 
4597
- export { CONFIG_FILE, DECISIONS_FORMAT_MARKER, DEFAULT_CONFIG, EXTRACTION_THRESHOLD, FLEET_JSON_SCHEMA, FLEET_LENS, LENSES, NAME, PACK_VERSION, PREMISE_BRIEF_FILE, PREMISE_LENS, TOTAL_BUDGET_WORDS, VERSION, applyFiles, baselineCarriesMachinePath, baselinePath, buildGateInventory, buildTruthEnv, cacheFactsPath, checkDecisionRefs, checkDocRefs, checkManifest, checkTextClaims, collectWallFindings, configPath, contextEconomyLens, corpPersistenceRoot, deriveProfile, emptyCounters, expandTilde, extractCommandClaims, extractPathClaims, gateIntegrityLens, instructionTruthLens, isAlwaysAppliedCursorRule, listInstructionFiles, loadDecisionLedger, loadFleetManifest, measureContext, planWorkflow, promoteBareTokens, rankFindings, readBaseline, readCachedFacts, readConfig, readLedger, reconcileLedger, renderBrief, runAudit, runPremise, scanProject, stateFreshnessLens, sweepFleet, withoutMachinePath, writeBaseline, writeCachedFacts, writeLedger };
4990
+ export { CONFIG_FILE, DECISIONS_FORMAT_MARKER, DEFAULT_CONFIG, EXTRACTION_THRESHOLD, FLEET_JSON_SCHEMA, FLEET_LENS, LENSES, NAME, PACK_VERSION, PREMISE_BRIEF_FILE, PREMISE_LENS, PROPOSAL_SCHEMA, RUBRIC_CRITERIA, TOTAL_BUDGET_WORDS, VERSION, applyFiles, baselineCarriesMachinePath, baselinePath, buildGateInventory, buildProposals, buildTruthEnv, cacheFactsPath, checkDecisionRefs, checkDocRefs, checkManifest, checkTextClaims, collectWallFindings, configPath, contextEconomyLens, deriveProfile, emptyCounters, expandTilde, extractCommandClaims, extractPathClaims, gateIntegrityLens, guardedPersistenceRoot, instructionTruthLens, isAlwaysAppliedCursorRule, listInstructionFiles, loadDecisionLedger, loadFleetManifest, measureContext, parseRubric, planWorkflow, promoteBareTokens, rankFindings, readBaseline, readCachedFacts, readConfig, readLedger, reconcileLedger, renderBrief, runAudit, runPremise, scanProject, stateFreshnessLens, sweepFleet, withoutMachinePath, writeBaseline, writeCachedFacts, writeLedger };