@deftai/directive 0.58.0 → 0.59.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.
@@ -13,6 +13,8 @@ export declare const CLI_MODULE_VERBS: readonly ["agents-refresh", "cache", "che
13
13
  export declare const CORE_MODULE_VERBS: readonly ["scm", "github-auth-modes", "github-body", "issue-emit", "issue-ingest", "reconcile-issues", "swarm-launch", "swarm-complete-cohort", "swarm-readiness", "swarm-routing-verify", "swarm-routing-set", "swarm-verify-review-clean", "swarm-worktrees", "framework-commands", "pack-render", "packs-slice", "prd-render", "project-render", "roadmap-render", "spec-render", "spec-validate", "code-structure-validate", "pack-migrate-skills", "pack-migrate-rules", "pack-migrate-strategies", "pack-migrate-patterns", "pack-migrate-swarm-spec", "policy-set", "scope-undo", "scope-demote", "scope-decompose", "changelog-resolve-unreleased", "architecture-preflight-sor"];
14
14
  /** Task-style aliases (framework_commands / Taskfile names). */
15
15
  export declare const VERB_ALIASES: Readonly<Record<string, string>>;
16
+ /** Native `policy-set` dispatcher (replaces the policy_set.py shell-out, #2022 Phase 1). */
17
+ export declare function runPolicySet(argv: string[], io: DispatchIo): number;
16
18
  /** Resolve a user-facing verb to its canonical handler key. */
17
19
  export declare function resolveCanonicalVerb(verb: string): string | null;
18
20
  /** Sorted list of all registered verb names (canonical + aliases). */
package/dist/dispatch.js CHANGED
@@ -2,9 +2,12 @@
2
2
  * Unified `directive <verb> [args]` dispatcher (#1828 s0).
3
3
  * Routes to ported command modules in packages/cli and packages/core.
4
4
  */
5
- import { execFileSync } from "node:child_process";
6
- import { join, resolve } from "node:path";
5
+ import { existsSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
6
+ import { homedir } from "node:os";
7
+ import { basename, dirname, isAbsolute, join, relative, resolve } from "node:path";
7
8
  import { engineInfo } from "@deftai/directive-core";
9
+ import { appendAuditLog, disclosureLine, projectDefinitionPath, resolvePolicy, resolveWipCap, setPolicy, } from "@deftai/directive-core/policy";
10
+ import { KNOWN_SUBAGENT_BACKEND_IDS, probeSubagentBackends, resolveSwarmSubagentBackend, } from "@deftai/directive-core/swarm";
8
11
  const HANDLER_KEYS = [
9
12
  "run",
10
13
  "main",
@@ -324,23 +327,1164 @@ function parseCodeStructureArgs(argv) {
324
327
  }
325
328
  return { projectRoot, paths, json, strict };
326
329
  }
327
- function loadPythonScriptHandler(scriptName) {
328
- return (argv) => {
329
- const deftRoot = resolveDeftRoot();
330
- try {
331
- execFileSync("uv", ["--project", deftRoot, "run", "python", join(deftRoot, "scripts", scriptName), ...argv], {
332
- cwd: deftRoot,
333
- encoding: "utf8",
334
- env: { ...process.env, PYTHONUTF8: "1", DEFT_CACHE_DISABLE: "1" },
335
- stdio: "inherit",
336
- });
337
- return 0;
338
- }
339
- catch (err) {
340
- const e = err;
341
- return typeof e.status === "number" ? e.status : 1;
330
+ // ===========================================================================
331
+ // Native pack-migrate handlers (#2022 Phase 1).
332
+ //
333
+ // Port of scripts/pack_migrate_{skills,rules,strategies,patterns,swarm_spec}.py
334
+ // to native TypeScript so the pack-render surface no longer shells into bundled
335
+ // Python. Output parity with the Python scripts is exact, including the
336
+ // json.dumps(..., indent=2, ensure_ascii=True) + "\n" serialization, document
337
+ // scanning order, and per-entry field ordering.
338
+ // ===========================================================================
339
+ const PACK_VERSION = "0.1";
340
+ const DEFAULT_SKILL_VERSION = "0.1";
341
+ const SHOULD_NOT_GLYPH = "\u2249";
342
+ const MUST_NOT_GLYPH = "\u2297";
343
+ /** Serialize like Python json.dumps(value, indent=2, ensure_ascii=True) + "\n". */
344
+ function dumpsAsciiJson(value) {
345
+ const base = JSON.stringify(value, null, 2);
346
+ let out = "";
347
+ for (let i = 0; i < base.length; i += 1) {
348
+ const code = base.charCodeAt(i);
349
+ // ensure_ascii escapes every code unit outside the printable ASCII range
350
+ // (0x20-0x7e). JSON.stringify has already escaped control chars (< 0x20)
351
+ // and the structural quote/backslash, so only chars > 0x7e remain literal.
352
+ if (code > 0x7e) {
353
+ out += `\\u${code.toString(16).padStart(4, "0")}`;
342
354
  }
355
+ else {
356
+ out += base.charAt(i);
357
+ }
358
+ }
359
+ return `${out}\n`;
360
+ }
361
+ /** Strip leading/trailing chars in `chars` (Python str.strip(chars)); whitespace when omitted. */
362
+ function pyStrip(value, chars) {
363
+ if (chars === undefined) {
364
+ return value.replace(/^\s+/, "").replace(/\s+$/, "");
365
+ }
366
+ let start = 0;
367
+ let end = value.length;
368
+ while (start < end && chars.includes(value.charAt(start)))
369
+ start += 1;
370
+ while (end > start && chars.includes(value.charAt(end - 1)))
371
+ end -= 1;
372
+ return value.slice(start, end);
373
+ }
374
+ // Python str.splitlines() universal newlines: \n \r \r\n \v \f \x1c \x1d \x1e \x85 \u2028 \u2029.
375
+ // Built from code points (as \uXXXX escape text) so no literal control characters land in the source.
376
+ const LINE_BOUNDARY_CLASS = [0x0a, 0x0d, 0x0b, 0x0c, 0x1c, 0x1d, 0x1e, 0x85, 0x2028, 0x2029]
377
+ .map((code) => `\\u${code.toString(16).padStart(4, "0")}`)
378
+ .join("");
379
+ const LINE_BOUNDARY_RE = new RegExp(`\\r\\n|[${LINE_BOUNDARY_CLASS}]`);
380
+ /** Mirror Python str.splitlines(): split on universal line boundaries, dropping one terminal break. */
381
+ function splitLines(text) {
382
+ if (text === "")
383
+ return [];
384
+ const parts = text.split(LINE_BOUNDARY_RE);
385
+ if (parts.length > 0 && parts[parts.length - 1] === "")
386
+ parts.pop();
387
+ return parts;
388
+ }
389
+ /** Repo-relative POSIX path of `to` measured from `from`. */
390
+ function relPosix(from, to) {
391
+ return relative(from, to).split(/[\\/]/).join("/");
392
+ }
393
+ /** Python Path.stem -- filename minus its final suffix. */
394
+ function stemOf(filePath) {
395
+ const base = basename(filePath);
396
+ const dot = base.lastIndexOf(".");
397
+ return dot > 0 ? base.slice(0, dot) : base;
398
+ }
399
+ /** Slugify a doc stem: lowercase, runs of non-alnum -> '-', trimmed of '-'. */
400
+ function slugify(stem) {
401
+ return pyStrip(stem.toLowerCase().replace(/[^a-z0-9]+/g, "-"), "-");
402
+ }
403
+ function isFileSafe(path) {
404
+ try {
405
+ return statSync(path).isFile();
406
+ }
407
+ catch {
408
+ return false;
409
+ }
410
+ }
411
+ function isDirSafe(path) {
412
+ try {
413
+ return statSync(path).isDirectory();
414
+ }
415
+ catch {
416
+ return false;
417
+ }
418
+ }
419
+ /** Sorted SKILL.md paths one directory below skillsDir (Python skills_dir glob of the SKILL.md docs). */
420
+ function globSkillMd(skillsDir) {
421
+ const out = [];
422
+ let names;
423
+ try {
424
+ names = readdirSync(skillsDir);
425
+ }
426
+ catch {
427
+ return out;
428
+ }
429
+ for (const name of names) {
430
+ const dir = join(skillsDir, name);
431
+ if (!isDirSafe(dir))
432
+ continue;
433
+ const candidate = join(dir, "SKILL.md");
434
+ if (isFileSafe(candidate))
435
+ out.push(candidate);
436
+ }
437
+ out.sort((a, b) => (a < b ? -1 : a > b ? 1 : 0));
438
+ return out;
439
+ }
440
+ /** Sorted full paths of `<dir>/*.md` (Python dir.glob("*.md")). */
441
+ function globMd(dir) {
442
+ const out = [];
443
+ let names;
444
+ try {
445
+ names = readdirSync(dir);
446
+ }
447
+ catch {
448
+ return out;
449
+ }
450
+ for (const name of names) {
451
+ if (!name.endsWith(".md"))
452
+ continue;
453
+ const candidate = join(dir, name);
454
+ if (isFileSafe(candidate))
455
+ out.push(candidate);
456
+ }
457
+ out.sort((a, b) => (a < b ? -1 : a > b ? 1 : 0));
458
+ return out;
459
+ }
460
+ const H1_RE = /^#\s+(.+?)\s*$/;
461
+ const CHROME_PREFIXES = [
462
+ "legend ",
463
+ "legend(",
464
+ "**legend",
465
+ `**${"\u26a0\ufe0f"}`,
466
+ "**see also",
467
+ "<!--",
468
+ ];
469
+ function isChrome(line) {
470
+ const low = line.replace(/^\s+/, "").toLowerCase();
471
+ if (CHROME_PREFIXES.some((prefix) => low.startsWith(prefix)))
472
+ return true;
473
+ const stripped = line.trim();
474
+ return stripped.length > 0 && [...stripped].every((ch) => ch === "-" || ch === "=");
475
+ }
476
+ /** Index a line array with a defined fallback (`i` is always in range at call sites). */
477
+ function lineAt(lines, i) {
478
+ return lines[i] ?? "";
479
+ }
480
+ function extractTitle(md) {
481
+ for (const line of splitLines(md)) {
482
+ const match = H1_RE.exec(line);
483
+ if (match)
484
+ return (match[1] ?? "").trim();
485
+ }
486
+ return "";
487
+ }
488
+ function extractDescription(md) {
489
+ const lines = splitLines(md);
490
+ const n = lines.length;
491
+ let i = 0;
492
+ while (i < n && !H1_RE.test(lineAt(lines, i)))
493
+ i += 1;
494
+ if (i < n)
495
+ i += 1;
496
+ while (i < n && (lineAt(lines, i).trim() === "" || isChrome(lineAt(lines, i))))
497
+ i += 1;
498
+ const block = [];
499
+ while (i < n &&
500
+ lineAt(lines, i).trim() !== "" &&
501
+ !lineAt(lines, i).replace(/^\s+/, "").startsWith("#")) {
502
+ let stripped = lineAt(lines, i).trim();
503
+ if (stripped.startsWith(">"))
504
+ stripped = stripped.replace(/^>+/, "").trim();
505
+ if (stripped)
506
+ block.push(stripped);
507
+ i += 1;
508
+ }
509
+ return block.join(" ");
510
+ }
511
+ const REDIRECT_MARKERS = [
512
+ "legacy alias",
513
+ "superseded",
514
+ "has been renamed",
515
+ "has moved",
516
+ "deprecated",
517
+ ];
518
+ function isRedirectStub(md) {
519
+ const lines = splitLines(md);
520
+ const n = lines.length;
521
+ let i = 0;
522
+ while (i < n && !H1_RE.test(lineAt(lines, i)))
523
+ i += 1;
524
+ if (i < n)
525
+ i += 1;
526
+ while (i < n && (lineAt(lines, i).trim() === "" || isChrome(lineAt(lines, i))))
527
+ i += 1;
528
+ if (i >= n || !lineAt(lines, i).replace(/^\s+/, "").startsWith(">"))
529
+ return false;
530
+ const block = [];
531
+ while (i < n && lineAt(lines, i).replace(/^\s+/, "").startsWith(">")) {
532
+ block.push(lineAt(lines, i).replace(/^\s+/, "").replace(/^>+/, "").trim());
533
+ i += 1;
534
+ }
535
+ const quote = block.join(" ").toLowerCase();
536
+ return REDIRECT_MARKERS.some((marker) => quote.includes(marker));
537
+ }
538
+ const BANNER_OPEN = "<!-- AUTO-GENERATED by task packs:render";
539
+ function stripLeadingBanner(body) {
540
+ const lines = body.split("\n");
541
+ const n = lines.length;
542
+ let i = 0;
543
+ while (i < n && lineAt(lines, i).trim() === "")
544
+ i += 1;
545
+ if (i < n && lineAt(lines, i).startsWith(BANNER_OPEN)) {
546
+ while (i < n && lineAt(lines, i).replace(/^\s+/, "").startsWith("<!--"))
547
+ i += 1;
548
+ while (i < n && lineAt(lines, i).trim() === "")
549
+ i += 1;
550
+ }
551
+ return lines.slice(i).join("\n");
552
+ }
553
+ const FRONTMATTER_RE = /^---\n([\s\S]*?\n)---\n?([\s\S]*)$/;
554
+ function splitFrontmatter(text) {
555
+ if (!text.startsWith("---\n"))
556
+ return [null, text];
557
+ const match = FRONTMATTER_RE.exec(text);
558
+ if (!match)
559
+ return [null, text];
560
+ return [match[1] ?? "", match[2] ?? ""];
561
+ }
562
+ function foldBlock(blockLines) {
563
+ const paragraphs = [];
564
+ let current = [];
565
+ for (const line of blockLines) {
566
+ if (line.trim() === "") {
567
+ if (current.length) {
568
+ paragraphs.push(current.join(" "));
569
+ current = [];
570
+ }
571
+ }
572
+ else {
573
+ current.push(line.trim());
574
+ }
575
+ }
576
+ if (current.length)
577
+ paragraphs.push(current.join(" "));
578
+ return paragraphs.join("\n");
579
+ }
580
+ const KEY_RE = /^([A-Za-z_][\w-]*):(.*)$/;
581
+ const BLOCK_INDICATORS = new Set([">", ">-", ">+", "|", "|-", "|+"]);
582
+ function isIndented(line) {
583
+ return line.startsWith(" ") || line.startsWith("\t");
584
+ }
585
+ function parseFrontmatterFields(frontmatter) {
586
+ const lines = frontmatter.split("\n");
587
+ const fields = {};
588
+ const n = lines.length;
589
+ let i = 0;
590
+ while (i < n) {
591
+ const line = lineAt(lines, i);
592
+ const match = KEY_RE.exec(line);
593
+ if (!match || isIndented(line)) {
594
+ i += 1;
595
+ continue;
596
+ }
597
+ const key = match[1] ?? "";
598
+ const value = (match[2] ?? "").trim();
599
+ if (BLOCK_INDICATORS.has(value)) {
600
+ const block = [];
601
+ i += 1;
602
+ while (i < n) {
603
+ const nxt = lineAt(lines, i);
604
+ if (nxt.trim() === "") {
605
+ block.push("");
606
+ i += 1;
607
+ continue;
608
+ }
609
+ if (isIndented(nxt)) {
610
+ block.push(nxt);
611
+ i += 1;
612
+ continue;
613
+ }
614
+ break;
615
+ }
616
+ fields[key] = foldBlock(block);
617
+ continue;
618
+ }
619
+ if (value === "" || value.startsWith("- ")) {
620
+ i += 1;
621
+ while (i < n &&
622
+ (lineAt(lines, i).replace(/^\s+/, "").startsWith("- ") || isIndented(lineAt(lines, i)))) {
623
+ i += 1;
624
+ }
625
+ if (!(key in fields))
626
+ fields[key] = "";
627
+ continue;
628
+ }
629
+ fields[key] = pyStrip(pyStrip(value, '"'), "'");
630
+ i += 1;
631
+ }
632
+ return fields;
633
+ }
634
+ function extractExtraFrontmatter(frontmatter) {
635
+ const lines = frontmatter.split("\n");
636
+ const extra = [];
637
+ const n = lines.length;
638
+ let i = 0;
639
+ while (i < n) {
640
+ const line = lineAt(lines, i);
641
+ const match = KEY_RE.exec(line);
642
+ if (!match || isIndented(line)) {
643
+ i += 1;
644
+ continue;
645
+ }
646
+ const key = match[1] ?? "";
647
+ const value = (match[2] ?? "").trim();
648
+ const block = [line];
649
+ i += 1;
650
+ if (BLOCK_INDICATORS.has(value)) {
651
+ while (i < n && (lineAt(lines, i).trim() === "" || isIndented(lineAt(lines, i)))) {
652
+ block.push(lineAt(lines, i));
653
+ i += 1;
654
+ }
655
+ }
656
+ else if (value === "" || value.startsWith("- ")) {
657
+ while (i < n &&
658
+ (lineAt(lines, i).replace(/^\s+/, "").startsWith("- ") || isIndented(lineAt(lines, i)))) {
659
+ block.push(lineAt(lines, i));
660
+ i += 1;
661
+ }
662
+ }
663
+ if (key !== "name" && key !== "description")
664
+ extra.push(...block);
665
+ }
666
+ while (extra.length && (extra[extra.length - 1] ?? "").trim() === "")
667
+ extra.pop();
668
+ return extra.length ? extra.join("\n") : null;
669
+ }
670
+ const ROUTING_HEADING = "## Skill Routing";
671
+ const ROUTING_PATH_RE = /`(?:content\/)?(skills\/[^`]+\/SKILL\.md)`/;
672
+ const ARROW_SPLIT_RE = /\u2192|->/;
673
+ function parseRouting(agentsMd) {
674
+ const mapping = new Map();
675
+ const start = agentsMd.indexOf(ROUTING_HEADING);
676
+ if (start === -1)
677
+ return mapping;
678
+ const rest = agentsMd.slice(start + ROUTING_HEADING.length);
679
+ const end = rest.indexOf("\n## ");
680
+ const section = end !== -1 ? rest.slice(0, end) : rest;
681
+ for (const raw of splitLines(section)) {
682
+ const line = raw.trim();
683
+ if (!line.startsWith("- "))
684
+ continue;
685
+ const pathMatch = ROUTING_PATH_RE.exec(line);
686
+ if (!pathMatch)
687
+ continue;
688
+ const path = pathMatch[1] ?? "";
689
+ const head = line.split(ARROW_SPLIT_RE)[0] ?? "";
690
+ const keywords = (head.match(/"[^"]+"/g) ?? []).map((quoted) => quoted.slice(1, -1));
691
+ let bucket = mapping.get(path);
692
+ if (!bucket) {
693
+ bucket = [];
694
+ mapping.set(path, bucket);
695
+ }
696
+ for (const keyword of keywords) {
697
+ if (!bucket.includes(keyword))
698
+ bucket.push(keyword);
699
+ }
700
+ }
701
+ return mapping;
702
+ }
703
+ function buildSkillEntry(skillMd, skillsDir, routing, captureBody) {
704
+ const text = readFileSync(skillMd, "utf8");
705
+ const [frontmatter, body] = splitFrontmatter(text);
706
+ if (frontmatter === null)
707
+ return null;
708
+ const fields = parseFrontmatterFields(frontmatter);
709
+ const name = (fields.name ?? "").trim();
710
+ if (!name)
711
+ return null;
712
+ const relPath = relPosix(dirname(resolve(skillsDir)), resolve(skillMd));
713
+ const triggers = routing.get(relPath) ?? [];
714
+ const version = (fields.version ?? "").trim() || DEFAULT_SKILL_VERSION;
715
+ return {
716
+ id: name,
717
+ description: (fields.description ?? "").trim(),
718
+ triggers,
719
+ path: relPath,
720
+ version,
721
+ body: captureBody ? stripLeadingBanner(body) : null,
722
+ frontmatter_extra: extractExtraFrontmatter(frontmatter),
723
+ };
724
+ }
725
+ function buildSkillsPack(skillsDir, agentsMd, proofSkill) {
726
+ const routing = parseRouting(readFileSync(agentsMd, "utf8"));
727
+ const captureAll = proofSkill === null;
728
+ const proofPath = proofSkill !== null ? `skills/${proofSkill}/SKILL.md` : null;
729
+ const base = dirname(resolve(skillsDir));
730
+ const skills = [];
731
+ for (const skillMd of globSkillMd(skillsDir)) {
732
+ const relPath = relPosix(base, resolve(skillMd));
733
+ const entry = buildSkillEntry(skillMd, skillsDir, routing, captureAll || relPath === proofPath);
734
+ if (entry !== null)
735
+ skills.push(entry);
736
+ }
737
+ return {
738
+ pack: "skills-pack-0.1",
739
+ version: PACK_VERSION,
740
+ generated_from: "skills/*/SKILL.md + AGENTS.md (Skill Routing)",
741
+ skills,
742
+ };
743
+ }
744
+ const GLYPH_TIER = {
745
+ "!": "MUST",
746
+ "~": "SHOULD",
747
+ [SHOULD_NOT_GLYPH]: "SHOULD_NOT",
748
+ [MUST_NOT_GLYPH]: "MUST_NOT",
749
+ "?": "MAY",
750
+ };
751
+ const MARKER_RE = new RegExp(`^\\s*(?:-\\s+)?([!~?${SHOULD_NOT_GLYPH}${MUST_NOT_GLYPH}])\\s+(\\S.*)$`);
752
+ const PROSE_TIERS = [
753
+ ["MUST NOT", "MUST_NOT"],
754
+ ["SHOULD NOT", "SHOULD_NOT"],
755
+ ["MUST", "MUST"],
756
+ ["SHOULD", "SHOULD"],
757
+ ["MAY", "MAY"],
758
+ ];
759
+ function proseTier(text) {
760
+ for (const [keyword, tier] of PROSE_TIERS) {
761
+ const pattern = new RegExp(`\\b${keyword.replace(/ /g, "[ ]")}\\b`);
762
+ if (pattern.test(text))
763
+ return tier;
764
+ }
765
+ return null;
766
+ }
767
+ function parseRules(md, domain) {
768
+ const rules = [];
769
+ let seq = 0;
770
+ for (const raw of splitLines(md)) {
771
+ const line = raw.replace(/\s+$/, "");
772
+ let tier = null;
773
+ let text = "";
774
+ const marker = MARKER_RE.exec(line);
775
+ if (marker) {
776
+ tier = GLYPH_TIER[marker[1] ?? ""] ?? null;
777
+ text = (marker[2] ?? "").trim();
778
+ }
779
+ else {
780
+ const stripped = line.trim();
781
+ if (!stripped.startsWith("- "))
782
+ continue;
783
+ text = stripped.slice(2).trim();
784
+ tier = text ? proseTier(text) : null;
785
+ }
786
+ if (tier === null || text === "")
787
+ continue;
788
+ seq += 1;
789
+ rules.push({ id: `${domain}-${String(seq).padStart(3, "0")}`, tier, domain, text });
790
+ }
791
+ return rules;
792
+ }
793
+ const MANAGED_SECTION_RE = /<!--\s*deft:managed-section[\s\S]*?<!--\s*\/deft:managed-section\s*-->/g;
794
+ function stripManagedSection(md) {
795
+ return md.replace(MANAGED_SECTION_RE, "");
796
+ }
797
+ function buildRulesPack(codingDir, extraSources) {
798
+ const base = dirname(resolve(codingDir));
799
+ const rules = [];
800
+ for (const md of globMd(codingDir)) {
801
+ const relPath = relPosix(base, resolve(md));
802
+ const domain = slugify(stemOf(md));
803
+ const text = readFileSync(md, "utf8");
804
+ const docRules = parseRules(text, domain);
805
+ docRules.forEach((rule, idx) => {
806
+ rule.path = relPath;
807
+ rule.body = idx === 0 ? stripLeadingBanner(text) : null;
808
+ rules.push(rule);
809
+ });
810
+ }
811
+ for (const src of extraSources) {
812
+ if (!isFileSafe(src))
813
+ continue;
814
+ const candidate = relPosix(base, resolve(src));
815
+ const relPath = candidate.startsWith("..") || isAbsolute(candidate) ? basename(src) : candidate;
816
+ const domain = slugify(stemOf(src));
817
+ let text = readFileSync(src, "utf8");
818
+ if (basename(src) === "AGENTS.md")
819
+ text = stripManagedSection(text);
820
+ for (const rule of parseRules(text, domain)) {
821
+ rule.path = relPath;
822
+ rule.body = null;
823
+ rules.push(rule);
824
+ }
825
+ }
826
+ return {
827
+ pack: "rules-pack-0.1",
828
+ version: PACK_VERSION,
829
+ generated_from: "coding/*.md + AGENTS.md + main.md (marker-prefixed RFC2119 directives; " +
830
+ "AGENTS.md managed-section excluded; coding bodies rendered, " +
831
+ "AGENTS.md/main.md metadata-only)",
832
+ rules,
833
+ };
834
+ }
835
+ function buildMdEntry(md, dir, captureBody) {
836
+ const relPath = relPosix(dirname(resolve(dir)), resolve(md));
837
+ const stemSlug = slugify(stemOf(md));
838
+ const text = readFileSync(md, "utf8");
839
+ return {
840
+ id: stemSlug,
841
+ title: extractTitle(text),
842
+ description: extractDescription(text),
843
+ triggers: stemSlug ? [stemSlug] : [],
844
+ path: relPath,
845
+ body: captureBody ? stripLeadingBanner(text) : null,
846
+ };
847
+ }
848
+ function buildStrategiesPack(strategiesDir, proofStrategy) {
849
+ const base = dirname(resolve(strategiesDir));
850
+ const captureAll = proofStrategy === null;
851
+ const strategies = [];
852
+ for (const md of globMd(strategiesDir)) {
853
+ const relPath = relPosix(base, resolve(md));
854
+ const captureBody = captureAll
855
+ ? !isRedirectStub(readFileSync(md, "utf8"))
856
+ : relPath === proofStrategy;
857
+ strategies.push(buildMdEntry(md, strategiesDir, captureBody));
858
+ }
859
+ return {
860
+ pack: "strategies-pack-0.1",
861
+ version: PACK_VERSION,
862
+ generated_from: "strategies/*.md",
863
+ strategies,
864
+ };
865
+ }
866
+ function buildPatternsPack(patternsDir, proofPattern) {
867
+ const base = dirname(resolve(patternsDir));
868
+ const patterns = [];
869
+ for (const md of globMd(patternsDir)) {
870
+ const relPath = relPosix(base, resolve(md));
871
+ patterns.push(buildMdEntry(md, patternsDir, relPath === proofPattern));
872
+ }
873
+ return {
874
+ pack: "patterns-pack-0.1",
875
+ version: PACK_VERSION,
876
+ generated_from: "patterns/*.md",
877
+ patterns,
878
+ };
879
+ }
880
+ function buildSwarmSpecPack(swarmDir, proofEntry) {
881
+ const base = dirname(resolve(swarmDir));
882
+ const entries = [];
883
+ for (const md of globMd(swarmDir)) {
884
+ const relPath = relPosix(base, resolve(md));
885
+ entries.push(buildMdEntry(md, swarmDir, relPath === proofEntry));
886
+ }
887
+ return {
888
+ pack: "swarm-spec-pack-0.1",
889
+ version: PACK_VERSION,
890
+ generated_from: "swarm/*.md",
891
+ entries,
892
+ };
893
+ }
894
+ function writePack(out, pack) {
895
+ mkdirSync(dirname(out), { recursive: true });
896
+ writeFileSync(out, dumpsAsciiJson(pack), "utf8");
897
+ }
898
+ /** Resolve the shippable content root: <root>/content when present, else <root> (#1875). */
899
+ function resolveContentRoot() {
900
+ const root = resolveDeftRoot();
901
+ const candidate = join(root, "content");
902
+ return isDirSafe(candidate) ? candidate : root;
903
+ }
904
+ /**
905
+ * Minimal argparse-compatible option reader supporting `--flag value` and
906
+ * `--flag=value`. `listFlags` accumulate repeats; all flags take a value.
907
+ */
908
+ function parsePackArgs(argv, valueFlags, listFlags = []) {
909
+ const values = {};
910
+ const lists = {};
911
+ const known = new Set([...valueFlags, ...listFlags]);
912
+ for (let i = 0; i < argv.length; i += 1) {
913
+ const arg = argv[i];
914
+ if (arg === undefined)
915
+ continue;
916
+ let flag = arg;
917
+ let inlineValue;
918
+ if (arg.startsWith("--") && arg.includes("=")) {
919
+ const eq = arg.indexOf("=");
920
+ flag = arg.slice(0, eq);
921
+ inlineValue = arg.slice(eq + 1);
922
+ }
923
+ if (!known.has(flag)) {
924
+ return { values, lists, error: `unrecognized argument: ${arg}` };
925
+ }
926
+ let value = inlineValue;
927
+ if (value === undefined) {
928
+ i += 1;
929
+ value = argv[i];
930
+ }
931
+ if (value === undefined) {
932
+ return { values, lists, error: `argument ${flag}: expected one argument` };
933
+ }
934
+ if (listFlags.includes(flag)) {
935
+ const bucket = lists[flag] ?? [];
936
+ bucket.push(value);
937
+ lists[flag] = bucket;
938
+ }
939
+ else {
940
+ values[flag] = value;
941
+ }
942
+ }
943
+ return { values, lists };
944
+ }
945
+ function runPackMigrateSkills(argv, io) {
946
+ const contentRoot = resolveContentRoot();
947
+ const parsed = parsePackArgs(argv, ["--skills-dir", "--agents-md", "--proof-skill", "--out"]);
948
+ if (parsed.error !== undefined) {
949
+ io.writeErr(`error: ${parsed.error}\n`);
950
+ return 2;
951
+ }
952
+ const skillsDir = parsed.values["--skills-dir"] ?? join(contentRoot, "skills");
953
+ const agentsMd = parsed.values["--agents-md"] ?? join(resolveDeftRoot(), "AGENTS.md");
954
+ const proofSkill = parsed.values["--proof-skill"] ?? null;
955
+ const out = parsed.values["--out"] ?? join(contentRoot, "packs", "skills", "skills-pack-0.1.json");
956
+ if (!isDirSafe(skillsDir)) {
957
+ io.writeErr(`error: skills directory not found: ${skillsDir}\n`);
958
+ return 1;
959
+ }
960
+ if (!isFileSafe(agentsMd)) {
961
+ io.writeErr(`error: AGENTS.md not found: ${agentsMd}\n`);
962
+ return 1;
963
+ }
964
+ const pack = buildSkillsPack(skillsDir, agentsMd, proofSkill);
965
+ if (pack.skills.length === 0) {
966
+ io.writeErr(`error: no skills with frontmatter discovered under ${skillsDir}\n`);
967
+ return 1;
968
+ }
969
+ writePack(out, pack);
970
+ const bodied = pack.skills.filter((s) => s.body !== null).length;
971
+ io.writeOut(`Migrated ${pack.skills.length} skills (${bodied} with body) -> ${out}\n`);
972
+ return 0;
973
+ }
974
+ function runPackMigrateRules(argv, io) {
975
+ const contentRoot = resolveContentRoot();
976
+ const deftRoot = resolveDeftRoot();
977
+ const parsed = parsePackArgs(argv, ["--coding-dir", "--out"], ["--extra-source"]);
978
+ if (parsed.error !== undefined) {
979
+ io.writeErr(`error: ${parsed.error}\n`);
980
+ return 2;
981
+ }
982
+ const codingDir = parsed.values["--coding-dir"] ?? join(contentRoot, "coding");
983
+ const extraSources = parsed.lists["--extra-source"] ?? [
984
+ join(deftRoot, "AGENTS.md"),
985
+ join(deftRoot, "main.md"),
986
+ ];
987
+ const out = parsed.values["--out"] ?? join(contentRoot, "packs", "rules", "rules-pack-0.1.json");
988
+ if (!isDirSafe(codingDir)) {
989
+ io.writeErr(`error: coding directory not found: ${codingDir}\n`);
990
+ return 1;
991
+ }
992
+ const pack = buildRulesPack(codingDir, extraSources);
993
+ if (pack.rules.length === 0) {
994
+ io.writeErr(`error: no directives discovered under ${codingDir}\n`);
995
+ return 1;
996
+ }
997
+ writePack(out, pack);
998
+ const bodied = pack.rules.filter((r) => r.body != null).length;
999
+ io.writeOut(`Migrated ${pack.rules.length} rules (${bodied} with body) -> ${out}\n`);
1000
+ return 0;
1001
+ }
1002
+ function runPackMigrateStrategies(argv, io) {
1003
+ const contentRoot = resolveContentRoot();
1004
+ const parsed = parsePackArgs(argv, ["--strategies-dir", "--proof-strategy", "--out"]);
1005
+ if (parsed.error !== undefined) {
1006
+ io.writeErr(`error: ${parsed.error}\n`);
1007
+ return 2;
1008
+ }
1009
+ const strategiesDir = parsed.values["--strategies-dir"] ?? join(contentRoot, "strategies");
1010
+ const proofStrategy = parsed.values["--proof-strategy"] ?? null;
1011
+ const out = parsed.values["--out"] ?? join(contentRoot, "packs", "strategies", "strategies-pack-0.1.json");
1012
+ if (!isDirSafe(strategiesDir)) {
1013
+ io.writeErr(`error: strategies directory not found: ${strategiesDir}\n`);
1014
+ return 1;
1015
+ }
1016
+ const pack = buildStrategiesPack(strategiesDir, proofStrategy);
1017
+ if (pack.strategies.length === 0) {
1018
+ io.writeErr(`error: no strategies discovered under ${strategiesDir}\n`);
1019
+ return 1;
1020
+ }
1021
+ writePack(out, pack);
1022
+ const bodied = pack.strategies.filter((s) => s.body !== null).length;
1023
+ io.writeOut(`Migrated ${pack.strategies.length} strategies (${bodied} with body) -> ${out}\n`);
1024
+ return 0;
1025
+ }
1026
+ function runPackMigratePatterns(argv, io) {
1027
+ const contentRoot = resolveContentRoot();
1028
+ const parsed = parsePackArgs(argv, ["--patterns-dir", "--proof-pattern", "--out"]);
1029
+ if (parsed.error !== undefined) {
1030
+ io.writeErr(`error: ${parsed.error}\n`);
1031
+ return 2;
1032
+ }
1033
+ const patternsDir = parsed.values["--patterns-dir"] ?? join(contentRoot, "patterns");
1034
+ const proofPattern = parsed.values["--proof-pattern"] ?? "patterns/multi-agent.md";
1035
+ const out = parsed.values["--out"] ?? join(contentRoot, "packs", "patterns", "patterns-pack-0.1.json");
1036
+ if (!isDirSafe(patternsDir)) {
1037
+ io.writeErr(`error: patterns directory not found: ${patternsDir}\n`);
1038
+ return 1;
1039
+ }
1040
+ const pack = buildPatternsPack(patternsDir, proofPattern);
1041
+ if (pack.patterns.length === 0) {
1042
+ io.writeErr(`error: no patterns discovered under ${patternsDir}\n`);
1043
+ return 1;
1044
+ }
1045
+ writePack(out, pack);
1046
+ const bodied = pack.patterns.filter((p) => p.body !== null).length;
1047
+ io.writeOut(`Migrated ${pack.patterns.length} patterns (${bodied} with body) -> ${out}\n`);
1048
+ return 0;
1049
+ }
1050
+ function runPackMigrateSwarmSpec(argv, io) {
1051
+ const contentRoot = resolveContentRoot();
1052
+ const parsed = parsePackArgs(argv, ["--swarm-dir", "--proof-entry", "--out"]);
1053
+ if (parsed.error !== undefined) {
1054
+ io.writeErr(`error: ${parsed.error}\n`);
1055
+ return 2;
1056
+ }
1057
+ const swarmDir = parsed.values["--swarm-dir"] ?? join(contentRoot, "swarm");
1058
+ const proofEntry = parsed.values["--proof-entry"] ?? "swarm/swarm.md";
1059
+ const out = parsed.values["--out"] ?? join(contentRoot, "packs", "swarm-spec", "swarm-spec-pack-0.1.json");
1060
+ if (!isDirSafe(swarmDir)) {
1061
+ io.writeErr(`error: swarm directory not found: ${swarmDir}\n`);
1062
+ return 1;
1063
+ }
1064
+ const pack = buildSwarmSpecPack(swarmDir, proofEntry);
1065
+ if (pack.entries.length === 0) {
1066
+ io.writeErr(`error: no swarm-spec docs discovered under ${swarmDir}\n`);
1067
+ return 1;
1068
+ }
1069
+ writePack(out, pack);
1070
+ const bodied = pack.entries.filter((e) => e.body !== null).length;
1071
+ io.writeOut(`Migrated ${pack.entries.length} swarm-spec entries (${bodied} with body) -> ${out}\n`);
1072
+ return 0;
1073
+ }
1074
+ // ===========================================================================
1075
+ // Native policy-set handler (#2022 Phase 1).
1076
+ //
1077
+ // Port of scripts/policy_set.py to native TypeScript so the typed-policy write
1078
+ // path (enforce-branches / allow-direct-commits / wip-cap / subagent-backend)
1079
+ // and the subagent-backends probe surface no longer shell into bundled Python.
1080
+ // Behaviour parity with the Python script is preserved: the audit row appended
1081
+ // to meta/policy-changes.log, the json.dumps(..., indent=2, ensure_ascii=False)
1082
+ // + "\n" serialization, the disclosure text, and the exit codes
1083
+ // (0 success / 1 refusal / 2 config-or-parse error).
1084
+ // ===========================================================================
1085
+ const POLICY_CAPABILITY_COST_DISCLOSURE = "\u26a0 Capability-cost disclosure -- enabling direct commits to the default " +
1086
+ "branch turns OFF the deft branch-protection policy.\n" +
1087
+ " \u2022 Pre-commit + pre-push hooks will no longer block default-branch " +
1088
+ "commits.\n" +
1089
+ " \u2022 verify:branch will pass on the default branch.\n" +
1090
+ " \u2022 The CI sanity check (head_ref != base_ref) is still independent and " +
1091
+ "will continue to flag master->master PRs.\n" +
1092
+ " \u2022 This change is reversible: run `task policy:enforce-branches` to " +
1093
+ "re-enable the gate.\n" +
1094
+ " \u2022 The change is recorded to meta/policy-changes.log for auditability.";
1095
+ const POLICY_WIP_CAP_DISCLOSURE = "\u26a0 Capability-cost disclosure -- changing plan.policy.wipCap " +
1096
+ "alters the refusal threshold on task scope:promote (#1124 / D4 of #1119).\n" +
1097
+ " \u2022 Raising the cap lets more vBRIEFs sit in pending/+active/ " +
1098
+ "before promotion is refused.\n" +
1099
+ " \u2022 Lowering the cap may put the project over cap immediately; " +
1100
+ "use `task scope:demote` / `task scope:demote --batch --older-than-days 30` " +
1101
+ "to drain.\n" +
1102
+ " \u2022 cap=0 freezes promotion entirely (useful for code-freeze " +
1103
+ "windows; restore by setting a positive value).\n" +
1104
+ " \u2022 This change is reversible and recorded to " +
1105
+ "meta/policy-changes.log for auditability.";
1106
+ const POLICY_SET_COMMANDS = [
1107
+ "enforce-branches",
1108
+ "allow-direct-commits",
1109
+ "wip-cap",
1110
+ "subagent-backend",
1111
+ "subagent-backends",
1112
+ ];
1113
+ /** Flags each subcommand accepts (mirrors the policy_set.py argparse subparsers). */
1114
+ const POLICY_SET_ALLOWED_FLAGS = {
1115
+ "enforce-branches": new Set(["--actor", "--note", "--project-root"]),
1116
+ "allow-direct-commits": new Set(["--confirm", "--actor", "--note", "--project-root"]),
1117
+ "wip-cap": new Set(["--set", "--confirm", "--actor", "--note", "--project-root"]),
1118
+ "subagent-backend": new Set(["--set", "--actor", "--note", "--project-root"]),
1119
+ "subagent-backends": new Set(["--format", "--project-root"]),
1120
+ };
1121
+ /** Custom error so write helpers can distinguish a missing file from a config fault. */
1122
+ class PolicySetError extends Error {
1123
+ kind;
1124
+ constructor(message, kind) {
1125
+ super(message);
1126
+ this.name = "PolicySetError";
1127
+ this.kind = kind;
1128
+ }
1129
+ }
1130
+ /** Python repr() for the audit-trail `previous=` field (None / 'str' / int / bool). */
1131
+ function pyRepr(value) {
1132
+ if (value === undefined || value === null)
1133
+ return "None";
1134
+ if (typeof value === "string")
1135
+ return `'${value}'`;
1136
+ if (typeof value === "boolean")
1137
+ return value ? "True" : "False";
1138
+ return String(value);
1139
+ }
1140
+ /** Strip newlines so an audit note stays a single log line (mirrors policy_set.py). */
1141
+ function sanitizeNote(note) {
1142
+ return note.replace(/\n/g, " ").replace(/\r/g, " ");
1143
+ }
1144
+ function defaultPolicySetActor(cmd) {
1145
+ switch (cmd) {
1146
+ case "enforce-branches":
1147
+ return "task policy:enforce-branches";
1148
+ case "allow-direct-commits":
1149
+ return "task policy:allow-direct-commits";
1150
+ case "wip-cap":
1151
+ return "task policy:wip-cap";
1152
+ case "subagent-backend":
1153
+ return "task policy:subagent-backend";
1154
+ case "subagent-backends":
1155
+ return "task policy:subagent-backends";
1156
+ }
1157
+ }
1158
+ /** Mirror Python `Path(...).expanduser()` for a leading `~` / `~/` segment. */
1159
+ function expandUser(p) {
1160
+ if (p === "~")
1161
+ return homedir();
1162
+ if (p.startsWith("~/") || p.startsWith("~\\")) {
1163
+ return join(homedir(), p.slice(2));
1164
+ }
1165
+ return p;
1166
+ }
1167
+ function policySetError(message) {
1168
+ return {
1169
+ cmd: "enforce-branches",
1170
+ confirm: false,
1171
+ actor: "",
1172
+ note: "",
1173
+ projectRoot: ".",
1174
+ format: "text",
1175
+ error: message,
1176
+ };
1177
+ }
1178
+ /** Parse `policy-set <cmd> [flags]` (mirrors the policy_set.py argparse surface). */
1179
+ function parsePolicySetArgs(argv) {
1180
+ const cmd = argv[0];
1181
+ if (cmd === undefined) {
1182
+ return policySetError("the following arguments are required: cmd");
1183
+ }
1184
+ if (!POLICY_SET_COMMANDS.includes(cmd)) {
1185
+ return policySetError(`argument cmd: invalid choice: '${cmd}'`);
1186
+ }
1187
+ const command = cmd;
1188
+ const allowed = POLICY_SET_ALLOWED_FLAGS[command];
1189
+ const args = {
1190
+ cmd: command,
1191
+ confirm: false,
1192
+ actor: defaultPolicySetActor(command),
1193
+ note: "",
1194
+ projectRoot: ".",
1195
+ format: "text",
1196
+ };
1197
+ const rest = argv.slice(1);
1198
+ for (let i = 0; i < rest.length; i += 1) {
1199
+ const token = rest[i];
1200
+ if (token === undefined)
1201
+ continue;
1202
+ let flag = token;
1203
+ let inlineValue;
1204
+ if (token.startsWith("--") && token.includes("=")) {
1205
+ const eq = token.indexOf("=");
1206
+ flag = token.slice(0, eq);
1207
+ inlineValue = token.slice(eq + 1);
1208
+ }
1209
+ if (!allowed.has(flag)) {
1210
+ return policySetError(`unrecognized arguments: ${token}`);
1211
+ }
1212
+ const takeValue = () => {
1213
+ if (inlineValue !== undefined)
1214
+ return inlineValue;
1215
+ i += 1;
1216
+ return rest[i];
1217
+ };
1218
+ if (flag === "--confirm") {
1219
+ args.confirm = true;
1220
+ continue;
1221
+ }
1222
+ const value = takeValue();
1223
+ if (value === undefined) {
1224
+ return policySetError(`argument ${flag}: expected one argument`);
1225
+ }
1226
+ if (flag === "--actor") {
1227
+ args.actor = value;
1228
+ }
1229
+ else if (flag === "--note") {
1230
+ args.note = value;
1231
+ }
1232
+ else if (flag === "--project-root") {
1233
+ args.projectRoot = expandUser(value);
1234
+ }
1235
+ else if (flag === "--format") {
1236
+ if (value !== "text" && value !== "json") {
1237
+ return policySetError(`argument --format: invalid choice: '${value}'`);
1238
+ }
1239
+ args.format = value;
1240
+ }
1241
+ else if (flag === "--set") {
1242
+ if (command === "wip-cap") {
1243
+ // Python int() strips surrounding whitespace, so "--set ' 5'" parsed
1244
+ // cleanly; trim before the integer check to preserve that contract.
1245
+ const capText = value.trim();
1246
+ if (!/^[+-]?\d+$/.test(capText)) {
1247
+ return policySetError(`argument --set: invalid int value: '${value}'`);
1248
+ }
1249
+ args.cap = Number.parseInt(capText, 10);
1250
+ }
1251
+ else {
1252
+ // subagent-backend --set <choice>
1253
+ if (!KNOWN_SUBAGENT_BACKEND_IDS.has(value)) {
1254
+ const choices = [...KNOWN_SUBAGENT_BACKEND_IDS].sort().join(", ");
1255
+ return policySetError(`argument --set: invalid choice: '${value}' (choose from ${choices})`);
1256
+ }
1257
+ args.backendId = value;
1258
+ }
1259
+ }
1260
+ }
1261
+ if (command === "wip-cap" && args.cap === undefined) {
1262
+ return policySetError("the following arguments are required: --set");
1263
+ }
1264
+ if (command === "subagent-backend" && args.backendId === undefined) {
1265
+ return policySetError("the following arguments are required: --set");
1266
+ }
1267
+ return args;
1268
+ }
1269
+ /** Load PROJECT-DEFINITION for an in-place typed-field write (mirrors the .setdefault chain). */
1270
+ function loadProjectDefinitionForWrite(projectRoot) {
1271
+ const path = projectDefinitionPath(projectRoot);
1272
+ if (!existsSync(path)) {
1273
+ throw new PolicySetError(`PROJECT-DEFINITION not found at ${path}`, "not-found");
1274
+ }
1275
+ let parsed;
1276
+ try {
1277
+ parsed = JSON.parse(readFileSync(path, "utf8"));
1278
+ }
1279
+ catch (err) {
1280
+ throw new PolicySetError(`PROJECT-DEFINITION at ${path} is not valid JSON: ${String(err)}`, "config");
1281
+ }
1282
+ // JSON.parse can yield a non-object top level (null / array / scalar) without
1283
+ // throwing; reject it before the .plan/.policy property chain dereferences it.
1284
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
1285
+ throw new PolicySetError(`PROJECT-DEFINITION at ${path} is not a JSON object`, "config");
1286
+ }
1287
+ const data = parsed;
1288
+ let plan = data.plan;
1289
+ if (plan === undefined) {
1290
+ plan = {};
1291
+ data.plan = plan;
1292
+ }
1293
+ if (typeof plan !== "object" || plan === null || Array.isArray(plan)) {
1294
+ throw new PolicySetError("PROJECT-DEFINITION 'plan' is not an object", "config");
1295
+ }
1296
+ const planObj = plan;
1297
+ let policy = planObj.policy;
1298
+ if (policy === undefined) {
1299
+ policy = {};
1300
+ planObj.policy = policy;
1301
+ }
1302
+ if (typeof policy !== "object" || policy === null || Array.isArray(policy)) {
1303
+ throw new PolicySetError("plan.policy is not an object", "config");
1304
+ }
1305
+ return { path, data, policyBlock: policy };
1306
+ }
1307
+ /** Write plan.policy.wipCap in place + append the audit row (mirrors set_wip_cap). */
1308
+ function writeWipCap(projectRoot, cap, actor, note) {
1309
+ const { path, data, policyBlock } = loadProjectDefinitionForWrite(projectRoot);
1310
+ const previous = policyBlock.wipCap;
1311
+ policyBlock.wipCap = cap;
1312
+ writeFileSync(path, `${JSON.stringify(data, null, 2)}\n`, "utf8");
1313
+ const changed = previous !== cap;
1314
+ const parts = [`actor=${actor}`, `wipCap=${cap}`, `previous=${pyRepr(previous)}`];
1315
+ if (note)
1316
+ parts.push(`note=${sanitizeNote(note)}`);
1317
+ const auditEntry = parts.join(" ");
1318
+ appendAuditLog(projectRoot, auditEntry);
1319
+ return { changed, auditEntry };
1320
+ }
1321
+ /** Write plan.policy.swarmSubagentBackend in place + append the audit row. */
1322
+ function writeSubagentBackend(projectRoot, backendId, actor, note) {
1323
+ const { path, data, policyBlock } = loadProjectDefinitionForWrite(projectRoot);
1324
+ const previous = policyBlock.swarmSubagentBackend;
1325
+ policyBlock.swarmSubagentBackend = backendId;
1326
+ writeFileSync(path, `${JSON.stringify(data, null, 2)}\n`, "utf8");
1327
+ const changed = previous !== backendId;
1328
+ const parts = [
1329
+ `actor=${actor}`,
1330
+ `swarmSubagentBackend=${backendId}`,
1331
+ `previous=${pyRepr(previous)}`,
1332
+ ];
1333
+ if (note)
1334
+ parts.push(`note=${sanitizeNote(note)}`);
1335
+ const auditEntry = parts.join(" ");
1336
+ appendAuditLog(projectRoot, auditEntry);
1337
+ return { changed, auditEntry };
1338
+ }
1339
+ /** Serialise the probe output for `subagent-backends --format json`. */
1340
+ function subagentBackendsToJson(backends) {
1341
+ const payload = {
1342
+ backends: backends.map((entry) => ({
1343
+ id: entry.backend_id,
1344
+ display_name: entry.display_name,
1345
+ roles: [...entry.roles],
1346
+ available: entry.available,
1347
+ })),
343
1348
  };
1349
+ return JSON.stringify(payload, null, 2);
1350
+ }
1351
+ /** Map a write-path error to its fail-closed message + exit code (mirrors the except blocks). */
1352
+ function reportPolicyWriteError(err, io) {
1353
+ const message = err instanceof Error ? err.message : String(err);
1354
+ if (err instanceof PolicySetError && err.kind === "not-found") {
1355
+ io.writeErr(`\u274c ${message}\n`);
1356
+ io.writeErr(" Recovery: run `task setup` to generate vbrief/PROJECT-DEFINITION.vbrief.json.\n");
1357
+ return 2;
1358
+ }
1359
+ io.writeErr(`\u274c Config error: ${message}\n`);
1360
+ return 2;
1361
+ }
1362
+ function applyBranchPolicy(args, io) {
1363
+ let target;
1364
+ if (args.cmd === "enforce-branches") {
1365
+ target = false;
1366
+ }
1367
+ else {
1368
+ if (!args.confirm) {
1369
+ io.writeOut(`${POLICY_CAPABILITY_COST_DISCLOSURE}\n`);
1370
+ io.writeOut("\n");
1371
+ io.writeOut("Re-run with --confirm to apply: task policy:allow-direct-commits -- --confirm\n");
1372
+ return 1;
1373
+ }
1374
+ target = true;
1375
+ }
1376
+ let result;
1377
+ try {
1378
+ result = setPolicy(args.projectRoot, {
1379
+ allowDirectCommits: target,
1380
+ actor: args.actor,
1381
+ note: args.note,
1382
+ });
1383
+ }
1384
+ catch (err) {
1385
+ const message = err instanceof Error ? err.message : String(err);
1386
+ if (message.includes("PROJECT-DEFINITION not found")) {
1387
+ io.writeErr(`\u274c ${message}\n`);
1388
+ io.writeErr(" Recovery: run `task setup` to generate vbrief/PROJECT-DEFINITION.vbrief.json.\n");
1389
+ return 2;
1390
+ }
1391
+ io.writeErr(`\u274c Config error: ${message}\n`);
1392
+ return 2;
1393
+ }
1394
+ const state = target ? "OFF" : "ON";
1395
+ io.writeOut(`\u2713 plan.policy.allowDirectCommitsToMaster=${target ? "true" : "false"} ` +
1396
+ `(branch-protection ${state}).\n`);
1397
+ if (result.changed) {
1398
+ io.writeOut(` audit: meta/policy-changes.log :: ${result.auditEntry}\n`);
1399
+ }
1400
+ else {
1401
+ io.writeOut(" no-op: value already matched (audit entry still appended for trail).\n");
1402
+ }
1403
+ io.writeOut(`${disclosureLine(resolvePolicy(args.projectRoot))}\n`);
1404
+ return 0;
1405
+ }
1406
+ function applyWipCap(args, io) {
1407
+ const cap = args.cap ?? 0;
1408
+ if (cap < 0) {
1409
+ io.writeErr(`\u274c --set must be >= 0; got ${cap}.\n`);
1410
+ return 1;
1411
+ }
1412
+ if (!args.confirm) {
1413
+ io.writeOut(`${POLICY_WIP_CAP_DISCLOSURE}\n`);
1414
+ io.writeOut("\n");
1415
+ io.writeOut(`Re-run with --confirm to apply: task policy:wip-cap -- --set ${cap} --confirm\n`);
1416
+ return 1;
1417
+ }
1418
+ let res;
1419
+ try {
1420
+ res = writeWipCap(args.projectRoot, cap, args.actor, args.note);
1421
+ }
1422
+ catch (err) {
1423
+ return reportPolicyWriteError(err, io);
1424
+ }
1425
+ io.writeOut(`\u2713 plan.policy.wipCap=${cap}.\n`);
1426
+ if (res.changed) {
1427
+ io.writeOut(` audit: meta/policy-changes.log :: ${res.auditEntry}\n`);
1428
+ }
1429
+ else {
1430
+ io.writeOut(" no-op: value already matched (audit entry still appended for trail).\n");
1431
+ }
1432
+ const result = resolveWipCap(args.projectRoot);
1433
+ io.writeOut(`[deft policy] plan.policy.wipCap=${result.cap} (source: ${result.source}).\n`);
1434
+ return 0;
1435
+ }
1436
+ function applySubagentBackend(args, io) {
1437
+ const backendId = args.backendId ?? "";
1438
+ let res;
1439
+ try {
1440
+ res = writeSubagentBackend(args.projectRoot, backendId, args.actor, args.note);
1441
+ }
1442
+ catch (err) {
1443
+ return reportPolicyWriteError(err, io);
1444
+ }
1445
+ io.writeOut(`\u2713 plan.policy.swarmSubagentBackend=${backendId}.\n`);
1446
+ if (res.changed) {
1447
+ io.writeOut(` audit: meta/policy-changes.log :: ${res.auditEntry}\n`);
1448
+ }
1449
+ else {
1450
+ io.writeOut(" no-op: value already matched (audit entry still appended for trail).\n");
1451
+ }
1452
+ const result = resolveSwarmSubagentBackend(args.projectRoot);
1453
+ io.writeOut(`[deft policy] plan.policy.swarmSubagentBackend=${pyRepr(result.backend_id)} ` +
1454
+ `(source: ${result.source}).\n`);
1455
+ return 0;
1456
+ }
1457
+ function applySubagentBackends(args, io) {
1458
+ const entries = probeSubagentBackends();
1459
+ if (args.format === "json") {
1460
+ io.writeOut(`${subagentBackendsToJson(entries)}\n`);
1461
+ return 0;
1462
+ }
1463
+ for (const entry of entries) {
1464
+ const roles = entry.roles.join(", ");
1465
+ const avail = entry.available ? "available" : "unavailable";
1466
+ io.writeOut(`${entry.backend_id}\t${entry.display_name}\troles=[${roles}]\t${avail}\n`);
1467
+ }
1468
+ return 0;
1469
+ }
1470
+ /** Native `policy-set` dispatcher (replaces the policy_set.py shell-out, #2022 Phase 1). */
1471
+ export function runPolicySet(argv, io) {
1472
+ const args = parsePolicySetArgs(argv);
1473
+ if (args.error !== undefined) {
1474
+ io.writeErr(`policy-set: ${args.error}\n`);
1475
+ return 2;
1476
+ }
1477
+ switch (args.cmd) {
1478
+ case "enforce-branches":
1479
+ case "allow-direct-commits":
1480
+ return applyBranchPolicy(args, io);
1481
+ case "wip-cap":
1482
+ return applyWipCap(args, io);
1483
+ case "subagent-backend":
1484
+ return applySubagentBackend(args, io);
1485
+ case "subagent-backends":
1486
+ return applySubagentBackends(args, io);
1487
+ }
344
1488
  }
345
1489
  async function loadCoreModuleHandler(verb, io) {
346
1490
  switch (verb) {
@@ -449,17 +1593,17 @@ async function loadCoreModuleHandler(verb, io) {
449
1593
  };
450
1594
  }
451
1595
  case "pack-migrate-skills":
452
- return loadPythonScriptHandler("pack_migrate_skills.py");
1596
+ return (argv) => runPackMigrateSkills(argv, io);
453
1597
  case "pack-migrate-rules":
454
- return loadPythonScriptHandler("pack_migrate_rules.py");
1598
+ return (argv) => runPackMigrateRules(argv, io);
455
1599
  case "pack-migrate-strategies":
456
- return loadPythonScriptHandler("pack_migrate_strategies.py");
1600
+ return (argv) => runPackMigrateStrategies(argv, io);
457
1601
  case "pack-migrate-patterns":
458
- return loadPythonScriptHandler("pack_migrate_patterns.py");
1602
+ return (argv) => runPackMigratePatterns(argv, io);
459
1603
  case "pack-migrate-swarm-spec":
460
- return loadPythonScriptHandler("pack_migrate_swarm_spec.py");
1604
+ return (argv) => runPackMigrateSwarmSpec(argv, io);
461
1605
  case "policy-set":
462
- return loadPythonScriptHandler("policy_set.py");
1606
+ return (argv) => runPolicySet(argv, io);
463
1607
  case "scope-undo": {
464
1608
  const { undoMain } = await import("@deftai/directive-core/dist/scope/main.js");
465
1609
  return undoMain;
@@ -59,7 +59,12 @@ export function normaliseStdout(text) {
59
59
  .split("\n")
60
60
  .filter((line) => !line.startsWith("Using CPython") &&
61
61
  !line.startsWith("Creating virtual environment") &&
62
- !line.startsWith("Installed "))
62
+ !line.startsWith("Installed ") &&
63
+ // #2022: the TS doctor emits a pre-cutover status line that the Python
64
+ // oracle (scripts/doctor.py) never had ("without a Python port"). Filter
65
+ // it so this intentional TS-only addition does not register as parity
66
+ // divergence. Removed when scripts/doctor.py is purged with the gate.
67
+ !line.startsWith("Pre-cutover:"))
63
68
  .join("\n");
64
69
  }
65
70
  function runScenario(deftRoot, scenario) {
package/dist/doctor.js CHANGED
@@ -1,7 +1,18 @@
1
1
  #!/usr/bin/env node
2
2
  import { fileURLToPath } from "node:url";
3
+ import { parseDoctorFlags } from "@deftai/directive-core/dist/doctor/flags.js";
3
4
  import { cmdDoctor } from "@deftai/directive-core/dist/doctor/main.js";
5
+ import { renderPrecutoverLine } from "@deftai/directive-core/dist/vbrief-validate/precutover.js";
4
6
  export function run(argv) {
7
+ // #2022: surface pre-cutover (pre-v0.20 document model) migration state alongside the
8
+ // core doctor report. Only emit on a valid, human-readable invocation: suppressed under
9
+ // --json (so the machine-readable report stays valid), on --help, and when unknown flags
10
+ // are present (so an invalid invocation still mirrors the core error path exactly).
11
+ const flags = parseDoctorFlags(argv);
12
+ if (!flags.json && !flags.help && flags.unknown.length === 0) {
13
+ const projectRoot = flags.projectRoot ?? process.cwd();
14
+ process.stdout.write(`${renderPrecutoverLine(projectRoot)}\n`);
15
+ }
5
16
  return cmdDoctor(argv);
6
17
  }
7
18
  if (process.argv[1] !== undefined && fileURLToPath(import.meta.url) === process.argv[1]) {
@@ -118,7 +118,7 @@ export const ORCHESTRATION_CLI_COVERAGE_MAP = [
118
118
  {
119
119
  pythonTest: "test_release_subprocess_path.py",
120
120
  kind: "existing-coverage",
121
- tsTarget: "packages/core/src/release/python-bridge.test.ts",
121
+ tsTarget: "packages/core/src/release/python-steps.test.ts",
122
122
  },
123
123
  {
124
124
  pythonTest: "test_release_summary.py",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@deftai/directive",
3
- "version": "0.58.0",
3
+ "version": "0.59.0",
4
4
  "description": "Directive CLI — npm install path for the Deft Directive framework.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -42,8 +42,8 @@
42
42
  "provenance": true
43
43
  },
44
44
  "dependencies": {
45
- "@deftai/directive-core": "^0.58.0",
46
- "@deftai/directive-content": "^0.58.0"
45
+ "@deftai/directive-core": "^0.59.0",
46
+ "@deftai/directive-content": "^0.59.0"
47
47
  },
48
48
  "scripts": {
49
49
  "build": "tsc -b"