etymd 0.15.0 → 0.17.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.17.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.
@@ -1449,9 +1449,13 @@ async function planWorkflow(root, facts, opts) {
1449
1449
  commitFormat: opts.gateConfig?.commitFormat,
1450
1450
  allowWriting: opts.gateConfig?.allowWriting ?? []
1451
1451
  };
1452
+ const tier = deriveFailOn(facts, {
1453
+ failOn: gateConfig.failOn,
1454
+ explicit: opts.gateFailOnPinned ?? false
1455
+ });
1452
1456
  await add(
1453
1457
  ".githooks/pre-push",
1454
- generatePrePushHook(facts, gateConfig, selfBuild),
1458
+ generatePrePushHook(facts, { ...gateConfig, failOn: tier.failOn }, selfBuild),
1455
1459
  "Correctness gate (pre-push)",
1456
1460
  true
1457
1461
  );
@@ -2762,6 +2766,13 @@ var PLACEHOLDER_PREFIX_RE = /^(?:my|your)-/i;
2762
2766
  function isPlaceholderClaim(token) {
2763
2767
  return token.split("/").some((seg) => PLACEHOLDER_PREFIX_RE.test(seg) || PLACEHOLDER_SEGMENTS.has(seg.toLowerCase()));
2764
2768
  }
2769
+ var NAMESPACE_IDENT = "[A-Za-z][A-Za-z0-9-]{0,15}";
2770
+ var NAMESPACE_STOP = new Set(
2771
+ "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(" ")
2772
+ );
2773
+ function isNamespace(ident) {
2774
+ return Boolean(ident) && !NAMESPACE_STOP.has(ident.toLowerCase());
2775
+ }
2765
2776
  function claimContext(text, index) {
2766
2777
  const start = text.lastIndexOf("\n", index) + 1;
2767
2778
  const endRaw = text.indexOf("\n", index);
@@ -2781,8 +2792,9 @@ ${line}`;
2781
2792
  }
2782
2793
  return line;
2783
2794
  }
2784
- function extractPathClaims(text) {
2795
+ function extractPathClaims(text, opts = {}) {
2785
2796
  const prospectiveOnly = /* @__PURE__ */ new Map();
2797
+ const namespacedOnly = /* @__PURE__ */ new Map();
2786
2798
  const placeholder = /* @__PURE__ */ new Set();
2787
2799
  for (const m of text.matchAll(/`([^`\n]+)`/g)) {
2788
2800
  const token = m[1].trim();
@@ -2804,14 +2816,21 @@ function extractPathClaims(text) {
2804
2816
  }
2805
2817
  const prospective2 = CREATION_CONTEXT_RE.test(claimContext(text, m.index ?? 0));
2806
2818
  prospectiveOnly.set(claim, (prospectiveOnly.get(claim) ?? true) && prospective2);
2819
+ if (opts.namespaces) {
2820
+ const nsLead = new RegExp(`(${NAMESPACE_IDENT}):[ \\t]*$`).exec(text.slice(0, m.index ?? 0));
2821
+ const prefixed = isNamespace(nsLead?.[1]);
2822
+ namespacedOnly.set(claim, (namespacedOnly.get(claim) ?? true) && prefixed);
2823
+ }
2807
2824
  }
2808
2825
  const paths = [];
2809
2826
  const prospective = [];
2827
+ const namespaced = [];
2810
2828
  for (const [claim, only] of prospectiveOnly) {
2811
2829
  if (only) prospective.push(claim);
2830
+ else if (namespacedOnly.get(claim)) namespaced.push(claim);
2812
2831
  else paths.push(claim);
2813
2832
  }
2814
- return { paths, prospective, placeholder: [...placeholder] };
2833
+ return { paths, prospective, namespaced, placeholder: [...placeholder] };
2815
2834
  }
2816
2835
  var LOCAL_REF_LEADINS = new Set(
2817
2836
  "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 +2939,9 @@ function emptyCounters() {
2920
2939
  prospectiveSkipped: 0,
2921
2940
  placeholderSkipped: 0,
2922
2941
  qualifiedRefsSkipped: 0,
2923
- unresolvableRefs: 0
2942
+ unresolvableRefs: 0,
2943
+ namespacedSkipped: 0,
2944
+ outsideRepoSkipped: 0
2924
2945
  };
2925
2946
  }
2926
2947
  async function checkTextClaims(env, file, opts, counters) {
@@ -2958,9 +2979,12 @@ async function checkTextClaims(env, file, opts, counters) {
2958
2979
  confidence: "high"
2959
2980
  });
2960
2981
  }
2961
- const { paths, prospective, placeholder } = extractPathClaims(file.text);
2982
+ const { paths, prospective, placeholder, namespaced } = extractPathClaims(file.text, {
2983
+ namespaces: opts.treatNamespacedPrefixes
2984
+ });
2962
2985
  counters.prospectiveSkipped += prospective.length;
2963
2986
  counters.placeholderSkipped += placeholder.length;
2987
+ counters.namespacedSkipped += namespaced.length;
2964
2988
  const missing = [];
2965
2989
  for (const claim of paths) {
2966
2990
  if (await env.pathResolves(claim)) examined.push({ kind: "path", value: claim, exists: true });
@@ -2970,6 +2994,11 @@ async function checkTextClaims(env, file, opts, counters) {
2970
2994
  const gitignored = new Set((ignoredOut ?? "").split("\n").filter(Boolean));
2971
2995
  let pathFindings = 0;
2972
2996
  for (const claim of missing) {
2997
+ if (opts.rootedFirstSegments && !opts.rootedFirstSegments.has(claim.split("/")[0] ?? claim)) {
2998
+ counters.outsideRepoSkipped += 1;
2999
+ examined.push({ kind: "path", value: claim, exists: null });
3000
+ continue;
3001
+ }
2973
3002
  if (gitignored.has(claim)) {
2974
3003
  counters.gitignoredSkipped += 1;
2975
3004
  examined.push({ kind: "path", value: claim, exists: null });
@@ -3469,14 +3498,14 @@ function expandTilde(p) {
3469
3498
  if (p.startsWith("~/")) return path2.join(os.homedir(), p.slice(2));
3470
3499
  return p;
3471
3500
  }
3472
- var REBUILD_RECIPE = 'rebuild it beside the manifest: {"machineProfile":"corp","dirs":{"<name>":"~/path/to/worktree"}}';
3501
+ var REBUILD_RECIPE = 'rebuild it beside the manifest: {"machineProfile":"guarded","dirs":{"<name>":"~/path/to/worktree"}}';
3473
3502
  var SAFE_NAME = /^[^\s/\\:.][^\s/\\:]*$/;
3474
3503
  function rejectUnsafeName(name, where, manifest, manifestFile) {
3475
3504
  if (SAFE_NAME.test(name)) return false;
3476
3505
  manifest.problems.push({
3477
3506
  kind: "bad-shape",
3478
3507
  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)`
3508
+ 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
3509
  });
3481
3510
  return true;
3482
3511
  }
@@ -3517,7 +3546,7 @@ async function readLocal(localPath, problems) {
3517
3546
  }
3518
3547
  }
3519
3548
  function loadRegistryEntries(projects, manifest, manifestFile) {
3520
- const corpNames = [];
3549
+ const guardedNames = [];
3521
3550
  for (const [i, rawEntry] of projects.entries()) {
3522
3551
  const rec = asRecord(rawEntry);
3523
3552
  const name = rec ? asString(rec.name) : void 0;
@@ -3530,8 +3559,8 @@ function loadRegistryEntries(projects, manifest, manifestFile) {
3530
3559
  continue;
3531
3560
  }
3532
3561
  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") {
3562
+ const profile = rec.profile === "guarded" ? "guarded" : "personal";
3563
+ if (rec.profile !== void 0 && rec.profile !== "guarded" && rec.profile !== "personal") {
3535
3564
  manifest.problems.push({
3536
3565
  kind: "bad-shape",
3537
3566
  file: manifestFile,
@@ -3569,12 +3598,12 @@ function loadRegistryEntries(projects, manifest, manifestFile) {
3569
3598
  contract,
3570
3599
  links: asStringMap(rec.links)
3571
3600
  };
3572
- if (entry.profile === "corp" || entry.private) {
3573
- corpNames.push(name);
3601
+ if (entry.profile === "guarded" || entry.private) {
3602
+ guardedNames.push(name);
3574
3603
  if (!manifest.localPresent) {
3575
3604
  entry.unresolved = `registry.local.json is absent \u2014 ${REBUILD_RECIPE}`;
3576
3605
  } else if (manifest.machineProfile === "personal") {
3577
- entry.unresolved = 'corp entries do not resolve under machineProfile "personal" \u2014 absent on this machine by design';
3606
+ entry.unresolved = 'guarded entries do not resolve under machineProfile "personal" \u2014 absent on this machine by design';
3578
3607
  } else {
3579
3608
  const dir = manifest.localDirs[name];
3580
3609
  if (!dir) {
@@ -3590,11 +3619,11 @@ function loadRegistryEntries(projects, manifest, manifestFile) {
3590
3619
  }
3591
3620
  manifest.entries.push(entry);
3592
3621
  }
3593
- if (corpNames.length && !manifest.localPresent) {
3622
+ if (guardedNames.length && !manifest.localPresent) {
3594
3623
  manifest.problems.push({
3595
3624
  kind: "local-missing",
3596
3625
  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}`
3626
+ detail: `${guardedNames.length} guarded/private entr${guardedNames.length === 1 ? "y" : "ies"} (${guardedNames.join(", ")}) cannot resolve without it \u2014 ${REBUILD_RECIPE}`
3598
3627
  });
3599
3628
  }
3600
3629
  }
@@ -3643,7 +3672,7 @@ async function loadFleetManifest(manifestPath) {
3643
3672
  shape: "registry",
3644
3673
  manifestPath: abs,
3645
3674
  dir,
3646
- corpHosts: [],
3675
+ guardedHosts: [],
3647
3676
  labels: {},
3648
3677
  localDirs: {},
3649
3678
  localPath: path2.join(dir, "registry.local.json"),
@@ -3676,8 +3705,8 @@ async function loadFleetManifest(manifestPath) {
3676
3705
  manifest.localPresent = local.present;
3677
3706
  manifest.localDirs = asStringMap(local.data.dirs);
3678
3707
  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;
3708
+ manifest.guardedHosts = Array.isArray(local.data.guardedHosts) ? local.data.guardedHosts.filter((h) => typeof h === "string") : [];
3709
+ manifest.machineProfile = local.data.machineProfile === "guarded" || local.data.machineProfile === "personal" ? local.data.machineProfile : void 0;
3681
3710
  const rootRaw = asString(local.data.root) ?? asString(parsed.root);
3682
3711
  if (rootRaw) manifest.root = path2.resolve(dir, expandTilde(rootRaw));
3683
3712
  const orientationRec = asRecord(parsed.orientation);
@@ -3720,10 +3749,136 @@ async function loadFleetManifest(manifestPath) {
3720
3749
  init_config();
3721
3750
  init_facts();
3722
3751
  init_util();
3752
+
3753
+ // src/engine/milestones.ts
3754
+ var MILESTONE_COLUMNS = [
3755
+ "id",
3756
+ "milestone",
3757
+ "goal",
3758
+ "status",
3759
+ "next",
3760
+ "effort",
3761
+ "depends-on"
3762
+ ];
3763
+ var MILESTONE_GOALS = ["1", "2", "3"];
3764
+ var MILESTONE_STATUSES = ["planned", "active", "blocked", "done"];
3765
+ var MILESTONE_EFFORTS = ["S", "M", "L"];
3766
+ var NONE_MARKERS = /* @__PURE__ */ new Set(["", "\u2014", "-", "\u2013", "none"]);
3767
+ function splitRow(line) {
3768
+ const trimmed = line.trim().replace(/^\|/, "").replace(/\|$/, "");
3769
+ return trimmed.split("|").map((c) => c.trim());
3770
+ }
3771
+ function isSeparatorRow(line) {
3772
+ const cells = splitRow(line);
3773
+ return cells.length > 0 && cells.every((c) => /^:?-{1,}:?$/.test(c));
3774
+ }
3775
+ function firstTable(lines, startLine) {
3776
+ for (let i = startLine; i < lines.length - 1; i++) {
3777
+ const line = lines[i] ?? "";
3778
+ const next = lines[i + 1] ?? "";
3779
+ if (!line.trim().startsWith("|") || !isSeparatorRow(next)) continue;
3780
+ const header = splitRow(line).map((h) => h.toLowerCase());
3781
+ const rows = [];
3782
+ for (let j = i + 2; j < lines.length; j++) {
3783
+ const body = lines[j] ?? "";
3784
+ if (!body.trim().startsWith("|")) break;
3785
+ rows.push({ line: j + 1, cells: splitRow(body) });
3786
+ }
3787
+ return { header, rows, headerLine: i + 1 };
3788
+ }
3789
+ return null;
3790
+ }
3791
+ function parseList(cell) {
3792
+ if (NONE_MARKERS.has(cell.trim().toLowerCase())) return [];
3793
+ return cell.split(",").map((s) => s.trim()).filter(Boolean);
3794
+ }
3795
+ function checkColumns(table, expected, problems) {
3796
+ const got = table.header.join(" | ");
3797
+ const want = expected.join(" | ");
3798
+ if (got !== want) {
3799
+ problems.push(
3800
+ `line ${table.headerLine}: table columns are \`${got}\`; the standard is \`${want}\``
3801
+ );
3802
+ return false;
3803
+ }
3804
+ return true;
3805
+ }
3806
+ function checkVocabulary(line, field, value, allowed, problems) {
3807
+ if (allowed.includes(value)) return true;
3808
+ problems.push(
3809
+ `line ${line}: ${field} is \`${value}\`; allowed: ${allowed.map((a) => `\`${a}\``).join(", ")}`
3810
+ );
3811
+ return false;
3812
+ }
3813
+ function parseMilestones(text) {
3814
+ const problems = [];
3815
+ const lines = text.split(/\r?\n/);
3816
+ const headingIdx = lines.findIndex((l) => /^#\s/.test(l));
3817
+ if (headingIdx === -1 || lines[headingIdx]?.trim() !== "# Milestones") {
3818
+ problems.push(
3819
+ headingIdx === -1 ? "no heading: the file must start with `# Milestones`" : `line ${headingIdx + 1}: first heading is \`${lines[headingIdx]?.trim()}\`; it must be \`# Milestones\``
3820
+ );
3821
+ }
3822
+ const table = firstTable(lines, headingIdx === -1 ? 0 : headingIdx + 1);
3823
+ if (!table) {
3824
+ problems.push(
3825
+ `no milestones table: expected \`| ${MILESTONE_COLUMNS.join(" | ")} |\` after the heading`
3826
+ );
3827
+ return { rows: [], problems };
3828
+ }
3829
+ if (!checkColumns(table, MILESTONE_COLUMNS, problems)) return { rows: [], problems };
3830
+ const rows = [];
3831
+ const ids = /* @__PURE__ */ new Set();
3832
+ for (const { line, cells } of table.rows) {
3833
+ if (cells.length !== MILESTONE_COLUMNS.length) {
3834
+ problems.push(`line ${line}: ${cells.length} cells, expected ${MILESTONE_COLUMNS.length}`);
3835
+ continue;
3836
+ }
3837
+ const [id, milestone, goal, status, next, effort, deps] = cells;
3838
+ let ok = true;
3839
+ if (!/^M\d+$/.test(id)) {
3840
+ problems.push(`line ${line}: id \`${id}\` is not \`M<number>\``);
3841
+ ok = false;
3842
+ } else if (ids.has(id)) {
3843
+ problems.push(`line ${line}: id \`${id}\` is used twice`);
3844
+ ok = false;
3845
+ }
3846
+ if (!milestone) {
3847
+ problems.push(`line ${line}: milestone title is empty`);
3848
+ ok = false;
3849
+ }
3850
+ if (!next) {
3851
+ problems.push(`line ${line}: next step is empty \u2014 a milestone without a next step is a wish`);
3852
+ ok = false;
3853
+ }
3854
+ ok = checkVocabulary(line, "goal", goal, MILESTONE_GOALS, problems) && ok;
3855
+ ok = checkVocabulary(line, "status", status, MILESTONE_STATUSES, problems) && ok;
3856
+ ok = checkVocabulary(line, "effort", effort, MILESTONE_EFFORTS, problems) && ok;
3857
+ if (!ok) continue;
3858
+ ids.add(id);
3859
+ rows.push({
3860
+ id,
3861
+ milestone,
3862
+ goal,
3863
+ status,
3864
+ next,
3865
+ effort,
3866
+ dependsOn: parseList(deps)
3867
+ });
3868
+ }
3869
+ for (const r of rows) {
3870
+ for (const d of r.dependsOn) {
3871
+ if (!ids.has(d)) problems.push(`${r.id} depends on \`${d}\`, which is not in the table`);
3872
+ }
3873
+ }
3874
+ return { rows, problems };
3875
+ }
3876
+
3877
+ // src/engine/fleet.ts
3723
3878
  var pExecFile3 = promisify(execFile);
3724
3879
  var FLEET_LENS = "fleet-manifest";
3725
3880
  var FLEET_JSON_SCHEMA = "fleet-experimental-0.2";
3726
- var CORP_WALL_ARTIFACTS = ["PROJECT_CONTEXT.md", "DECISIONS.md"];
3881
+ var GUARDED_WALL_ARTIFACTS = ["PROJECT_CONTEXT.md", "DECISIONS.md"];
3727
3882
  function recurringClasses(projects) {
3728
3883
  const byClass = /* @__PURE__ */ new Map();
3729
3884
  for (const p of projects) {
@@ -3756,12 +3911,12 @@ function finding3(id, tier, claim, evidence, why, action) {
3756
3911
  confidence: "high"
3757
3912
  };
3758
3913
  }
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) {
3914
+ function guardedPersistenceRoot(manifest, name) {
3915
+ const guardedDir = path2.join(manifest.dir, "guarded");
3916
+ const root = path2.resolve(guardedDir, name);
3917
+ if (path2.dirname(root) !== guardedDir || root === guardedDir) {
3763
3918
  throw new Error(
3764
- `entry name ${JSON.stringify(name)} escapes the corp persistence zone \u2014 refusing to resolve a ledger root for it`
3919
+ `entry name ${JSON.stringify(name)} escapes the guarded persistence zone \u2014 refusing to resolve a ledger root for it`
3765
3920
  );
3766
3921
  }
3767
3922
  return root;
@@ -3794,7 +3949,7 @@ async function realpathOr(p) {
3794
3949
  return path2.resolve(p);
3795
3950
  }
3796
3951
  }
3797
- function emailMatchesCorpHosts(email, hosts) {
3952
+ function emailMatchesGuardedHosts(email, hosts) {
3798
3953
  const domain = email.split("@")[1]?.toLowerCase();
3799
3954
  if (!domain) return false;
3800
3955
  return hosts.some((h) => {
@@ -3845,7 +4000,7 @@ async function checkManifest(manifest) {
3845
4000
  );
3846
4001
  }
3847
4002
  seen.add(entry.name);
3848
- if (entry.profile !== "corp" && !entry.private && !entry.trust) {
4003
+ if (entry.profile !== "guarded" && !entry.private && !entry.trust) {
3849
4004
  const bad = entry.trustRaw;
3850
4005
  findings.push(
3851
4006
  finding3(
@@ -3883,14 +4038,14 @@ async function checkManifest(manifest) {
3883
4038
  )
3884
4039
  );
3885
4040
  }
3886
- if (entry.unresolved && manifest.localPresent && manifest.machineProfile !== "personal" && (entry.profile === "corp" || entry.private)) {
4041
+ if (entry.unresolved && manifest.localPresent && manifest.machineProfile !== "personal" && (entry.profile === "guarded" || entry.private)) {
3887
4042
  findings.push(
3888
4043
  finding3(
3889
4044
  `${FLEET_LENS}/dangling-dir:${entry.name}`,
3890
4045
  "gap",
3891
4046
  `\`${entry.name}\` has no usable directory mapping`,
3892
4047
  [`${localFile}: ${entry.unresolved}`],
3893
- "An unmapped corp entry silently drops out of every sweep on the machine that most needs it.",
4048
+ "An unmapped guarded entry silently drops out of every sweep on the machine that most needs it.",
3894
4049
  `Add a \`dirs\` mapping for \`${entry.name}\` in ${localFile}.`
3895
4050
  )
3896
4051
  );
@@ -3954,28 +4109,28 @@ async function checkManifest(manifest) {
3954
4109
  );
3955
4110
  }
3956
4111
  if (manifest.machineProfile === "personal") {
3957
- const corpCount = manifest.entries.filter((e) => e.profile === "corp" || e.private).length;
3958
- if (corpCount) {
4112
+ const guardedCount = manifest.entries.filter((e) => e.profile === "guarded" || e.private).length;
4113
+ if (guardedCount) {
3959
4114
  disclosures.push(
3960
- `machineProfile "personal": ${corpCount} corp entr${corpCount === 1 ? "y" : "ies"} deliberately absent on this machine \u2014 disclosed, not dangling.`
4115
+ `machineProfile "personal": ${guardedCount} guarded entr${guardedCount === 1 ? "y" : "ies"} deliberately absent on this machine \u2014 disclosed, not dangling.`
3961
4116
  );
3962
4117
  }
3963
4118
  }
3964
4119
  return { findings, disclosures };
3965
4120
  }
3966
- async function checkCorpWallArtifacts(entries, findings) {
4121
+ async function checkGuardedWallArtifacts(entries, findings) {
3967
4122
  for (const entry of entries) {
3968
- if (entry.profile !== "corp" || !entry.resolvedRoot) continue;
3969
- for (const artifact of CORP_WALL_ARTIFACTS) {
4123
+ if (entry.profile !== "guarded" || !entry.resolvedRoot) continue;
4124
+ for (const artifact of GUARDED_WALL_ARTIFACTS) {
3970
4125
  if (await pathExists(path2.join(entry.resolvedRoot, artifact))) {
3971
4126
  findings.push(
3972
4127
  finding3(
3973
- `${FLEET_LENS}/corp-artifact-in-repo:${entry.name}:${artifact}`,
4128
+ `${FLEET_LENS}/guarded-artifact-in-repo:${entry.name}:${artifact}`,
3974
4129
  "risk",
3975
- `corp entry \`${entry.name}\` carries ${artifact} inside its worktree`,
4130
+ `guarded entry \`${entry.name}\` carries ${artifact} inside its worktree`,
3976
4131
  [`${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.`
4132
+ "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.",
4133
+ `Move it to the fleet's guarded/${entry.name}/ zone and leave a pointer if the harness needs one.`
3979
4134
  )
3980
4135
  );
3981
4136
  }
@@ -3983,9 +4138,9 @@ async function checkCorpWallArtifacts(entries, findings) {
3983
4138
  }
3984
4139
  }
3985
4140
  async function checkCoverage(manifest, findings, disclosures) {
3986
- if (!manifest.corpHosts.length) {
4141
+ if (!manifest.guardedHosts.length) {
3987
4142
  disclosures.push(
3988
- "Coverage check skipped \u2014 no corpHosts in the local manifest, so a corp remote cannot be recognized."
4143
+ "Coverage check skipped \u2014 no guardedHosts in the local manifest, so a guarded remote cannot be recognized."
3989
4144
  );
3990
4145
  return;
3991
4146
  }
@@ -4006,16 +4161,16 @@ async function checkCoverage(manifest, findings, disclosures) {
4006
4161
  if (!await pathExists(path2.join(dir, ".git"))) continue;
4007
4162
  const remotes = await git(dir, ["config", "--get-regexp", "^remote\\..*\\.url"]);
4008
4163
  if (!remotes) continue;
4009
- const corpRemote = manifest.corpHosts.find((h) => remotes.includes(h));
4010
- if (corpRemote) {
4164
+ const guardedRemote = manifest.guardedHosts.find((h) => remotes.includes(h));
4165
+ if (guardedRemote) {
4011
4166
  findings.push(
4012
4167
  finding3(
4013
- `${FLEET_LENS}/unregistered-corp-remote:${d.name}`,
4168
+ `${FLEET_LENS}/unregistered-guarded-remote:${d.name}`,
4014
4169
  "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."
4170
+ `\`${d.name}\` under the fleet root has a guarded remote but is not registered`,
4171
+ [`${dir}: remote matches ${guardedRemote}`],
4172
+ "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.",
4173
+ "Register it as a guarded entry (opaque alias + local dirs mapping) or move it out of the fleet root."
4019
4174
  )
4020
4175
  );
4021
4176
  }
@@ -4063,7 +4218,7 @@ async function checkHygieneNeedles(manifest, findings, disclosures) {
4063
4218
  [
4064
4219
  ...Object.values(manifest.labels),
4065
4220
  ...Object.values(manifest.localDirs).map((d) => path2.basename(expandNeedleDir(d))),
4066
- ...manifest.corpHosts
4221
+ ...manifest.guardedHosts
4067
4222
  ].filter((n) => n.length >= 3)
4068
4223
  )
4069
4224
  ];
@@ -4090,7 +4245,7 @@ async function checkHygieneNeedles(manifest, findings, disclosures) {
4090
4245
  "risk",
4091
4246
  `${file} in public-repo \`${entry.name}\` carries a needle from the fleet's private local manifest`,
4092
4247
  [`${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.",
4248
+ "A public repo that names a private label, guarded directory, or guarded host joins the public and guarded identities for anyone who greps.",
4094
4249
  "Scrub the needle from the tracked file (rewrite history if it already shipped)."
4095
4250
  )
4096
4251
  );
@@ -4101,10 +4256,10 @@ async function checkHygieneNeedles(manifest, findings, disclosures) {
4101
4256
  function expandNeedleDir(dir) {
4102
4257
  return dir.replace(/\/+$/, "");
4103
4258
  }
4104
- async function checkCorpEmails(manifest, findings, disclosures) {
4105
- if (!manifest.corpHosts.length) {
4259
+ async function checkGuardedEmails(manifest, findings, disclosures) {
4260
+ if (!manifest.guardedHosts.length) {
4106
4261
  disclosures.push(
4107
- "Corp-email check skipped \u2014 no corpHosts in the local manifest, so a corp domain cannot be recognized."
4262
+ "Guarded-email check skipped \u2014 no guardedHosts in the local manifest, so a guarded domain cannot be recognized."
4108
4263
  );
4109
4264
  return;
4110
4265
  }
@@ -4113,21 +4268,23 @@ async function checkCorpEmails(manifest, findings, disclosures) {
4113
4268
  const emails = await git(entry.resolvedRoot, ["log", "-30", "--format=%ae"]);
4114
4269
  if (emails === null) {
4115
4270
  disclosures.push(
4116
- `Corp-email check could not run in \`${entry.name}\` (no readable commit history) \u2014 undetermined, not clean.`
4271
+ `Guarded-email check could not run in \`${entry.name}\` (no readable commit history) \u2014 undetermined, not clean.`
4117
4272
  );
4118
4273
  continue;
4119
4274
  }
4120
4275
  const lines = emails.split("\n").filter(Boolean);
4121
- const matched = [...new Set(lines.filter((e) => emailMatchesCorpHosts(e, manifest.corpHosts)))];
4276
+ const matched = [
4277
+ ...new Set(lines.filter((e) => emailMatchesGuardedHosts(e, manifest.guardedHosts)))
4278
+ ];
4122
4279
  if (matched.length) {
4123
4280
  const count = lines.filter((e) => matched.includes(e)).length;
4124
4281
  findings.push(
4125
4282
  finding3(
4126
- `${FLEET_LENS}/corp-email:${entry.name}`,
4283
+ `${FLEET_LENS}/guarded-email:${entry.name}`,
4127
4284
  "risk",
4128
- `recent commits on personal entry \`${entry.name}\` are authored with a corp-host email`,
4285
+ `recent commits on personal entry \`${entry.name}\` are authored with a guarded-host email`,
4129
4286
  [`${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.",
4287
+ "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
4288
  "Set the repo-local identity to the personal one and amend the unpushed commits."
4132
4289
  )
4133
4290
  );
@@ -4141,7 +4298,7 @@ async function checkGateDrift(manifest, findings, disclosures) {
4141
4298
  for (const entry of manifest.entries) {
4142
4299
  const root = entry.resolvedRoot;
4143
4300
  if (!root || !await isDirectory(root)) continue;
4144
- if (entry.profile === "corp" || entry.private) continue;
4301
+ if (entry.profile === "guarded" || entry.private) continue;
4145
4302
  if (entry.gates === "none") {
4146
4303
  disclosures.push(`${entry.name}: gates declared \`none\` \u2014 deliberately absent, not missing`);
4147
4304
  continue;
@@ -4154,8 +4311,13 @@ async function checkGateDrift(manifest, findings, disclosures) {
4154
4311
  );
4155
4312
  continue;
4156
4313
  }
4157
- const { config } = await readConfig2(root);
4158
- const planned = (await planWorkflow2(root, facts, { agents: false, gates: true, gateConfig: config.gates })).filter((f) => f.executable);
4314
+ const { config, explicit } = await readConfig2(root);
4315
+ const planned = (await planWorkflow2(root, facts, {
4316
+ agents: false,
4317
+ gates: true,
4318
+ gateConfig: config.gates,
4319
+ gateFailOnPinned: explicit.gatesFailOn
4320
+ })).filter((f) => f.executable);
4159
4321
  const missing = planned.filter((f) => !f.exists).map((f) => f.path);
4160
4322
  const differing = planned.filter((f) => f.exists && f.differs);
4161
4323
  const stale = differing.filter((f) => f.drift !== "edited").map((f) => f.path);
@@ -4194,11 +4356,11 @@ async function checkGateDrift(manifest, findings, disclosures) {
4194
4356
  async function collectWallFindings(manifest) {
4195
4357
  const findings = [];
4196
4358
  const disclosures = [];
4197
- await checkCorpWallArtifacts(manifest.entries, findings);
4359
+ await checkGuardedWallArtifacts(manifest.entries, findings);
4198
4360
  await checkCoverage(manifest, findings, disclosures);
4199
4361
  await checkManifestRepoMachinePaths(manifest, findings, disclosures);
4200
4362
  await checkHygieneNeedles(manifest, findings, disclosures);
4201
- await checkCorpEmails(manifest, findings, disclosures);
4363
+ await checkGuardedEmails(manifest, findings, disclosures);
4202
4364
  await checkGateDrift(manifest, findings, disclosures);
4203
4365
  return { findings, disclosures };
4204
4366
  }
@@ -4264,13 +4426,13 @@ async function sweepEntry(manifest, entry, opts) {
4264
4426
  );
4265
4427
  }
4266
4428
  }
4267
- const corp = entry.profile === "corp";
4268
- const optedIn = !corp && await pathExists(path2.join(root, ETYMD_DIR));
4429
+ const guarded = entry.profile === "guarded";
4430
+ const optedIn = !guarded && await pathExists(path2.join(root, ETYMD_DIR));
4269
4431
  const result = await runAudit(root, {
4270
4432
  kind: opts.kind,
4271
- persistLedger: !corp && opts.persistLedgers === true && optedIn,
4272
- readOnlyRoot: corp,
4273
- ledgerRoot: corp ? corpPersistenceRoot(manifest, entry.name) : void 0,
4433
+ persistLedger: !guarded && opts.persistLedgers === true && optedIn,
4434
+ readOnlyRoot: guarded,
4435
+ ledgerRoot: guarded ? guardedPersistenceRoot(manifest, entry.name) : void 0,
4274
4436
  stateBudgets: stateBudgetsFor(entry),
4275
4437
  upstreamRemote
4276
4438
  });
@@ -4278,11 +4440,43 @@ async function sweepEntry(manifest, entry, opts) {
4278
4440
  if (entry.contract.placement === "none") {
4279
4441
  project.findings = project.findings.filter((f) => !f.id.endsWith("/no-contract"));
4280
4442
  }
4443
+ const milestonesFile = entry.contract.milestones;
4444
+ if (milestonesFile && milestonesFile !== "none") {
4445
+ const text = await readText(path2.join(root, milestonesFile));
4446
+ if (text === null) {
4447
+ project.findings.push(
4448
+ finding3(
4449
+ `${FLEET_LENS}/milestones-missing:${entry.name}`,
4450
+ "gap",
4451
+ `\`${entry.name}\` declares milestones at \`${milestonesFile}\` and the file is absent`,
4452
+ [
4453
+ `${path2.basename(manifest.manifestPath)}: ${entry.name}.contract.milestones = ${milestonesFile}`
4454
+ ],
4455
+ "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.",
4456
+ 'Create it (heading `# Milestones`, then the standard table), or declare `milestones: "none"` if this project deliberately carries no plan.'
4457
+ )
4458
+ );
4459
+ } else {
4460
+ const doc = parseMilestones(text);
4461
+ doc.problems.forEach((problem, i) => {
4462
+ project.findings.push(
4463
+ finding3(
4464
+ `${FLEET_LENS}/milestones-shape:${entry.name}:${i + 1}`,
4465
+ "gap",
4466
+ `\`${entry.name}\` ${milestonesFile}: ${problem}`,
4467
+ [`${milestonesFile}`],
4468
+ "A milestones file the board cannot parse is a plan the fleet cannot see.",
4469
+ `Fix the table to the standard shape: \`| ${MILESTONE_COLUMNS.join(" | ")} |\`.`
4470
+ )
4471
+ );
4472
+ });
4473
+ }
4474
+ }
4281
4475
  for (const f of project.findings) project.counts[f.tier] += 1;
4282
4476
  project.stateAgeDays = stateAgeDays(result.facts);
4283
4477
  project.staleAfterDays = result.config.config.state.staleAfterDays;
4284
4478
  for (const [key, value] of Object.entries(entry.contract)) {
4285
- if (key === "placement" || !value) continue;
4479
+ if (key === "placement" || key === "milestones" || !value) continue;
4286
4480
  const exists = await pathExists(path2.join(root, value.replace(/#.*$/, "")));
4287
4481
  project.disclosures.push(
4288
4482
  `contract override \`${key}: ${value}\` \u2014 registered, existence-checked only (${exists ? "present" : "MISSING"})`
@@ -4325,6 +4519,179 @@ async function sweepFleet(manifest, opts = {}) {
4325
4519
  };
4326
4520
  }
4327
4521
 
4522
+ // src/engine/propose.ts
4523
+ var PROPOSAL_SCHEMA = "proposal/1";
4524
+ var RUBRIC_CRITERIA = ["severity", "economy", "confidence", "breadth"];
4525
+ function parseRubric(text, file) {
4526
+ const lines = [];
4527
+ const seen = /* @__PURE__ */ new Map();
4528
+ const rows = text.split(/\r?\n/);
4529
+ for (const [i, raw] of rows.entries()) {
4530
+ const line = raw.trim();
4531
+ if (!line || line.startsWith("#")) continue;
4532
+ const at = i + 1;
4533
+ const refuse = (why) => new Error(
4534
+ `${file}:${at} \`${raw.trim()}\` \u2014 ${why} (criteria: ${RUBRIC_CRITERIA.join(", ")})`
4535
+ );
4536
+ const sep = line.indexOf(":");
4537
+ if (sep < 0) throw refuse("expected `criterion: <weight>`");
4538
+ const criterion = line.slice(0, sep).trim();
4539
+ const weightRaw = line.slice(sep + 1).trim();
4540
+ if (!RUBRIC_CRITERIA.includes(criterion)) {
4541
+ throw refuse(`unknown criterion \`${criterion}\``);
4542
+ }
4543
+ if (!/^\d+$/.test(weightRaw) || Number(weightRaw) < 1) {
4544
+ throw refuse(`weight \`${weightRaw}\` is not a positive integer`);
4545
+ }
4546
+ const known = criterion;
4547
+ if (seen.has(known)) {
4548
+ throw new Error(
4549
+ `${file}:${at} \`${raw.trim()}\` \u2014 criterion \`${criterion}\` is already weighted on line ${seen.get(known)}; one line per criterion`
4550
+ );
4551
+ }
4552
+ seen.set(known, at);
4553
+ lines.push({ criterion: known, weight: Number(weightRaw) });
4554
+ }
4555
+ if (!lines.length) {
4556
+ throw new Error(`${file} carries no criterion lines \u2014 a rubric with no criteria scores nothing`);
4557
+ }
4558
+ return { lines };
4559
+ }
4560
+ var TIER_VALUE = { risk: 3, gap: 2, polish: 1 };
4561
+ var EFFORT_VALUE = { S: 3, M: 2, L: 1 };
4562
+ var CONFIDENCE_VALUE = { high: 3, medium: 2, low: 1 };
4563
+ function findingSubject(project, f) {
4564
+ return {
4565
+ id: `${project.name}:${f.id}`,
4566
+ kind: "finding",
4567
+ classId: f.id.split(":")[0] ?? f.id,
4568
+ tier: f.tier,
4569
+ effort: f.effort,
4570
+ confidence: f.confidence,
4571
+ projects: [project.name],
4572
+ action: f.action ?? "",
4573
+ evidence: f.evidence
4574
+ };
4575
+ }
4576
+ function classSubject(classId, members) {
4577
+ const pick = (order, at, dir) => {
4578
+ const values = members.map((m) => order[at(m.finding)]);
4579
+ const value = dir === 1 ? Math.max(...values) : Math.min(...values);
4580
+ return Object.entries(order).find(([, v]) => v === value)[0];
4581
+ };
4582
+ const actions = [...new Set(members.map((m) => m.finding.action).filter(Boolean))].sort();
4583
+ return {
4584
+ id: `class:${classId}`,
4585
+ kind: "class",
4586
+ classId,
4587
+ tier: pick(TIER_VALUE, (f) => f.tier, 1),
4588
+ effort: pick(EFFORT_VALUE, (f) => f.effort, -1),
4589
+ confidence: pick(CONFIDENCE_VALUE, (f) => f.confidence, -1),
4590
+ projects: [...new Set(members.map((m) => m.project.name))].sort(),
4591
+ action: actions.join("; "),
4592
+ evidence: members.flatMap((m) => m.finding.evidence)
4593
+ };
4594
+ }
4595
+ var CRITERION_VALUE = {
4596
+ severity: (s) => TIER_VALUE[s.tier],
4597
+ economy: (s) => EFFORT_VALUE[s.effort],
4598
+ confidence: (s) => CONFIDENCE_VALUE[s.confidence],
4599
+ breadth: (s) => Math.min(3, s.projects.length)
4600
+ };
4601
+ var FILENAME_RE = /^[^\s:]+\.[A-Za-z0-9]+$/;
4602
+ function extractFiles(evidence) {
4603
+ const files = /* @__PURE__ */ new Set();
4604
+ for (const line of evidence) {
4605
+ const token = line.split(/[\s:]/)[0];
4606
+ if (token && (token.includes("/") || FILENAME_RE.test(token))) files.add(token);
4607
+ }
4608
+ return [...files].sort();
4609
+ }
4610
+ function extractGates(files) {
4611
+ return files.filter((f) => f.startsWith(".githooks/") || f.startsWith(".github/workflows/"));
4612
+ }
4613
+ function reversibility(files) {
4614
+ if (!files.length) return "undetermined";
4615
+ const generated = (f) => f.startsWith(".etymd/") || f.startsWith(".githooks/");
4616
+ return files.every(generated) ? "regenerable" : "git-reversible";
4617
+ }
4618
+ function toProposal(s, rubric) {
4619
+ const matched = [];
4620
+ let score = 0;
4621
+ for (const line of rubric.lines) {
4622
+ const value = CRITERION_VALUE[line.criterion](s);
4623
+ score += line.weight * value;
4624
+ if (value >= 2) matched.push({ criterion: line.criterion, weight: line.weight, value });
4625
+ }
4626
+ const files = extractFiles(s.evidence);
4627
+ return {
4628
+ id: s.id,
4629
+ schema: PROPOSAL_SCHEMA,
4630
+ class: s.classId,
4631
+ kind: s.kind,
4632
+ projects: s.projects,
4633
+ action: s.action,
4634
+ effort: s.effort,
4635
+ confidence: s.confidence,
4636
+ score,
4637
+ matched,
4638
+ implications: {
4639
+ projects: s.projects,
4640
+ files,
4641
+ gates: extractGates(files),
4642
+ reversibility: reversibility(files)
4643
+ }
4644
+ };
4645
+ }
4646
+ function buildProposals(sweep, rubricName, rubric) {
4647
+ const disclosures = [];
4648
+ const personal = sweep.projects.filter((p) => p.profile === "personal");
4649
+ const guarded = sweep.projects.filter((p) => p.profile !== "personal");
4650
+ if (guarded.length) {
4651
+ disclosures.push(
4652
+ `guarded entries excluded from proposals: ${guarded.map((p) => p.name).sort().join(", ")}`
4653
+ );
4654
+ }
4655
+ const notAudited = personal.filter((p) => !p.resolvedRoot || p.unresolved);
4656
+ if (notAudited.length) {
4657
+ disclosures.push(
4658
+ `not audited, so not proposed: ${notAudited.map((p) => p.name).sort().join(", ")}`
4659
+ );
4660
+ }
4661
+ const subjects = [];
4662
+ for (const project of personal) {
4663
+ for (const f of project.findings) {
4664
+ if (f.kind === "improvement") subjects.push(findingSubject(project, f));
4665
+ }
4666
+ }
4667
+ const byClass = /* @__PURE__ */ new Map();
4668
+ for (const project of personal) {
4669
+ for (const f of project.findings) {
4670
+ const classId = f.id.split(":")[0] ?? f.id;
4671
+ const entry = byClass.get(classId) ?? [];
4672
+ entry.push({ project, finding: f });
4673
+ byClass.set(classId, entry);
4674
+ }
4675
+ }
4676
+ for (const [classId, members] of byClass) {
4677
+ if (new Set(members.map((m) => m.project.name)).size >= 2) {
4678
+ subjects.push(classSubject(classId, members));
4679
+ }
4680
+ }
4681
+ const proposals = subjects.map((s) => toProposal(s, rubric)).sort((a, b) => b.score - a.score || a.id.localeCompare(b.id));
4682
+ return {
4683
+ schema: PROPOSAL_SCHEMA,
4684
+ manifest: sweep.manifest,
4685
+ rubric: rubricName,
4686
+ criteria: rubric.lines,
4687
+ proposals,
4688
+ disclosures: [
4689
+ ...disclosures,
4690
+ "implications.files are path-shaped tokens extracted from finding evidence \u2014 evidence lines carrying no path are not represented there"
4691
+ ]
4692
+ };
4693
+ }
4694
+
4328
4695
  // src/engine/premise.ts
4329
4696
  init_facts();
4330
4697
  init_scan();
@@ -4347,7 +4714,8 @@ function promoteBareTokens(text, ctx) {
4347
4714
  bareInvocations: 0,
4348
4715
  proseScripts: 0,
4349
4716
  hostnameLike: 0,
4350
- unrootedDirs: 0
4717
+ unrootedDirs: 0,
4718
+ namespaced: 0
4351
4719
  };
4352
4720
  const promoted = text.split(CODE_SPAN_RE).map((segment, i) => i % 2 === 1 ? segment : promoteProse(segment, ctx, skips)).join("");
4353
4721
  return { text: promoted, skips };
@@ -4366,28 +4734,44 @@ function promoteProse(segment, ctx, skips) {
4366
4734
  }
4367
4735
  return `\`${core}\`${trail}`;
4368
4736
  });
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;
4737
+ const parts = withCommands.split(/(\s+)/);
4738
+ for (let i = 0; i < parts.length; i += 2) {
4739
+ parts[i] = promoteToken(parts[i], i >= 2 ? parts[i - 2] : "", ctx, skips);
4740
+ }
4741
+ return parts.join("");
4742
+ }
4743
+ function namespaceOf(core, prev) {
4744
+ const ns = new RegExp(`^(${NAMESPACE_IDENT}):`).exec(core)?.[1] ?? new RegExp(`^(${NAMESPACE_IDENT}):$`).exec(prev)?.[1];
4745
+ return ns && !NAMESPACE_STOP.has(ns.toLowerCase()) ? ns : null;
4746
+ }
4747
+ function promoteToken(token, prev, ctx, skips) {
4748
+ const m = WRAP_RE.exec(token);
4749
+ if (!m) return token;
4750
+ const [, lead = "", core = "", trail = ""] = m;
4751
+ if (!core || core.includes("://")) return token;
4752
+ const namespace = namespaceOf(core, prev);
4753
+ const body = namespace && core.startsWith(`${namespace}:`) ? core.slice(namespace.length + 1) : core;
4754
+ if (!PATH_TOKEN_RE.test(body)) return token;
4755
+ const first = body.slice(0, body.indexOf("/"));
4756
+ const hostSuffix = HOSTNAME_RE.exec(first)?.[1]?.toLowerCase();
4757
+ if (hostSuffix && !KNOWN_EXTENSIONS.has(hostSuffix)) {
4758
+ skips.hostnameLike += 1;
4759
+ return token;
4760
+ }
4761
+ if (body.endsWith("/")) {
4762
+ if (!ctx.rootedDirs.has(first)) {
4763
+ skips.unrootedDirs += 1;
4378
4764
  return token;
4379
4765
  }
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];
4766
+ } else {
4767
+ const ext = body.toLowerCase().match(/\.([a-z0-9]{1,8})$/)?.[1];
4388
4768
  if (!ext || !KNOWN_EXTENSIONS.has(ext)) return token;
4389
- return `${lead}\`${core}\`${trail}`;
4390
- });
4769
+ }
4770
+ if (namespace) {
4771
+ skips.namespaced += 1;
4772
+ return token;
4773
+ }
4774
+ return `${lead}\`${body}\`${trail}`;
4391
4775
  }
4392
4776
  async function listRootedDirs(env) {
4393
4777
  const dirs = /* @__PURE__ */ new Set();
@@ -4405,7 +4789,8 @@ async function runPremise(opts) {
4405
4789
  const facts = await scanProject(root);
4406
4790
  const env = await buildTruthEnv(root, facts);
4407
4791
  const counters = emptyCounters();
4408
- const promoted = promoteBareTokens(task, { rootedDirs: await listRootedDirs(env) });
4792
+ const rootedDirs = await listRootedDirs(env);
4793
+ const promoted = promoteBareTokens(task, { rootedDirs });
4409
4794
  const text = { path: TASK_LABEL, text: promoted.text };
4410
4795
  const findings = [];
4411
4796
  const disclosures = [];
@@ -4418,6 +4803,8 @@ async function runPremise(opts) {
4418
4803
  subject: "The task",
4419
4804
  missingPathTier: "risk",
4420
4805
  maxPathFindings: MAX_PATH_FINDINGS,
4806
+ rootedFirstSegments: rootedDirs,
4807
+ treatNamespacedPrefixes: true,
4421
4808
  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
4809
  actionCommand: "Fix the task before handing it over: name the real script, or restore it.",
4423
4810
  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 +4861,21 @@ async function runPremise(opts) {
4474
4861
  `${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
4862
  );
4476
4863
  }
4864
+ if (skips.namespaced) {
4865
+ disclosures.push(
4866
+ `${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.`
4867
+ );
4868
+ }
4869
+ if (counters.namespacedSkipped) {
4870
+ disclosures.push(
4871
+ `${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.`
4872
+ );
4873
+ }
4874
+ if (counters.outsideRepoSkipped) {
4875
+ disclosures.push(
4876
+ `${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.`
4877
+ );
4878
+ }
4477
4879
  if (counters.unverifiableCommands) {
4478
4880
  disclosures.push(
4479
4881
  `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 +4996,4 @@ wrong problem.
4594
4996
  init_version();
4595
4997
  init_version2();
4596
4998
 
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 };
4999
+ 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 };