@deftai/directive-core 0.90.0 → 0.92.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.
Files changed (101) hide show
  1. package/dist/cache/fetch.js +3 -2
  2. package/dist/cache/io.d.ts +13 -2
  3. package/dist/cache/io.js +36 -7
  4. package/dist/cache/operations.js +5 -3
  5. package/dist/doctor/main.js +37 -0
  6. package/dist/doctor/openclaw-skills.d.ts +42 -8
  7. package/dist/doctor/openclaw-skills.js +281 -38
  8. package/dist/eval/health.d.ts +10 -1
  9. package/dist/eval/health.js +16 -16
  10. package/dist/hooks/dispatcher.d.ts +6 -1
  11. package/dist/hooks/dispatcher.js +30 -0
  12. package/dist/init-deposit/gitignore.js +3 -0
  13. package/dist/init-deposit/hygiene.d.ts +43 -1
  14. package/dist/init-deposit/hygiene.js +112 -7
  15. package/dist/init-deposit/init-deposit.d.ts +5 -0
  16. package/dist/init-deposit/init-deposit.js +37 -2
  17. package/dist/init-deposit/scaffold.js +3 -1
  18. package/dist/intake/github-auth-modes.d.ts +1 -1
  19. package/dist/intake/github-auth-modes.js +25 -3
  20. package/dist/intake/issue-ingest-cli.js +13 -0
  21. package/dist/intake/reconcile-issues-cli.js +13 -0
  22. package/dist/orchestration/probe-session.d.ts +5 -1
  23. package/dist/orchestration/probe-session.js +24 -13
  24. package/dist/platform/agents-md.js +1 -1
  25. package/dist/policy/deft-directive-disable.d.ts +86 -0
  26. package/dist/policy/deft-directive-disable.js +167 -0
  27. package/dist/policy/delivery-branch.d.ts +33 -0
  28. package/dist/policy/delivery-branch.js +124 -0
  29. package/dist/policy/index.d.ts +2 -0
  30. package/dist/policy/index.js +17 -1
  31. package/dist/policy/plan-extensions.d.ts +10 -0
  32. package/dist/policy/plan-extensions.js +16 -0
  33. package/dist/pr-closing-keywords/detect.d.ts +13 -0
  34. package/dist/pr-closing-keywords/detect.js +73 -8
  35. package/dist/pr-closing-keywords/index.d.ts +2 -2
  36. package/dist/pr-closing-keywords/index.js +1 -1
  37. package/dist/pr-closing-keywords/main.js +119 -90
  38. package/dist/pr-closing-keywords/types.d.ts +10 -0
  39. package/dist/render/constants.d.ts +16 -0
  40. package/dist/render/constants.js +16 -1
  41. package/dist/render/export-spec.d.ts +7 -1
  42. package/dist/render/export-spec.js +51 -5
  43. package/dist/render/index.d.ts +3 -2
  44. package/dist/render/index.js +2 -2
  45. package/dist/render/roadmap-render.js +164 -60
  46. package/dist/render/scope-outlook.d.ts +5 -0
  47. package/dist/render/scope-outlook.js +3 -0
  48. package/dist/render/spec-render.d.ts +28 -2
  49. package/dist/render/spec-render.js +111 -14
  50. package/dist/render/text-utils.d.ts +6 -0
  51. package/dist/render/text-utils.js +23 -0
  52. package/dist/scm/binary.d.ts +3 -0
  53. package/dist/scm/binary.js +10 -2
  54. package/dist/scm/call.d.ts +5 -0
  55. package/dist/scm/call.js +5 -0
  56. package/dist/scm/index.d.ts +1 -0
  57. package/dist/scm/index.js +1 -0
  58. package/dist/scm/main.d.ts +5 -0
  59. package/dist/scm/main.js +27 -0
  60. package/dist/scm/readiness-cli.d.ts +24 -0
  61. package/dist/scm/readiness-cli.js +85 -0
  62. package/dist/scm/readiness.d.ts +104 -0
  63. package/dist/scm/readiness.js +421 -0
  64. package/dist/scope/batch-promote.d.ts +26 -0
  65. package/dist/scope/batch-promote.js +167 -0
  66. package/dist/scope/brief-io.d.ts +3 -1
  67. package/dist/scope/brief-io.js +5 -3
  68. package/dist/scope/delivery-evidence.d.ts +112 -0
  69. package/dist/scope/delivery-evidence.js +419 -0
  70. package/dist/scope/index.d.ts +2 -0
  71. package/dist/scope/index.js +2 -0
  72. package/dist/scope/main.d.ts +7 -0
  73. package/dist/scope/main.js +155 -9
  74. package/dist/scope/registry-artifact-sync.js +4 -1
  75. package/dist/scope/transition.d.ts +11 -1
  76. package/dist/scope/transition.js +30 -4
  77. package/dist/scope/wip-cap-check.d.ts +6 -0
  78. package/dist/scope/wip-cap-check.js +14 -2
  79. package/dist/session/ritual-sentinel.js +21 -12
  80. package/dist/session/session-start-hook.d.ts +3 -0
  81. package/dist/session/session-start-hook.js +21 -0
  82. package/dist/session/session-start.d.ts +7 -0
  83. package/dist/session/session-start.js +85 -0
  84. package/dist/swarm/complete-cohort.d.ts +20 -1
  85. package/dist/swarm/complete-cohort.js +56 -9
  86. package/dist/swarm/finalize-cohort-cli.js +9 -0
  87. package/dist/swarm/finalize-cohort.d.ts +8 -0
  88. package/dist/swarm/finalize-cohort.js +217 -21
  89. package/dist/triage/welcome/index.d.ts +1 -1
  90. package/dist/triage/welcome/index.js +1 -1
  91. package/dist/triage/welcome/onboard.js +7 -1
  92. package/dist/triage/welcome/prior-state.d.ts +7 -0
  93. package/dist/triage/welcome/prior-state.js +16 -2
  94. package/dist/triage/welcome/writers.d.ts +10 -0
  95. package/dist/triage/welcome/writers.js +72 -2
  96. package/dist/user-config/experimental-rules.d.ts +43 -0
  97. package/dist/user-config/experimental-rules.js +162 -0
  98. package/dist/user-config/index.d.ts +1 -0
  99. package/dist/user-config/index.js +1 -0
  100. package/dist/verify-source/contained-writes.js +1 -1
  101. package/package.json +3 -3
@@ -1,5 +1,5 @@
1
1
  import { existsSync, readdirSync, readFileSync } from "node:fs";
2
- import { join, resolve } from "node:path";
2
+ import { dirname, join, resolve } from "node:path";
3
3
  import { ContainedWriteError, containedWrite } from "../fs/contained-write.js";
4
4
  import { GhRestError, InvalidRepoError, restIssueView, runGhApi, } from "../scm/gh-rest.js";
5
5
  import { DEFAULT_BATCH_SIZE, DEFAULT_DELAY_MS } from "./constants.js";
@@ -45,7 +45,8 @@ export function writeOpenInventoryStamp(options) {
45
45
  fetched_at: utcIso(clock, fetchedAt),
46
46
  open_count: 0,
47
47
  };
48
- atomicWriteText(openInventoryStampPath(options.cacheRoot, options.source, options.repo), `${JSON.stringify(payload)}\n`);
48
+ // #3042: contain stamp write against project root (parent of cacheRoot).
49
+ atomicWriteText(openInventoryStampPath(options.cacheRoot, options.source, options.repo), `${JSON.stringify(payload)}\n`, { projectRoot: dirname(resolve(options.cacheRoot)) });
49
50
  }
50
51
  export const REST_MAX_PER_PAGE = 100;
51
52
  export const REST_PAGINATION_MAX_PAGES = 100;
@@ -1,9 +1,20 @@
1
1
  import { tmpdir } from "node:os";
2
+ export interface AtomicWriteTextOptions {
3
+ /**
4
+ * Project / checkout root used as the containment boundary (#3042).
5
+ * When set, refuses symlink parents and out-of-root targets before temp+rename.
6
+ * Prefer always passing this for product sinks (brief stay-path, agents refresh, cache).
7
+ * When omitted, falls back to parent-dir containment (legacy test / low-risk helpers).
8
+ */
9
+ readonly projectRoot?: string;
10
+ }
2
11
  /**
3
12
  * Write text via tempfile + rename (mirrors Python `_atomic_write_text`).
4
- * #2951 Phase 2: temp payload write uses containedWrite under the parent dir.
13
+ * #2951 Phase 2: temp payload write uses containedWrite.
14
+ * #3042: when `projectRoot` is set, containment root is projectRoot (not dirname(path))
15
+ * so force-added lifecycle / cache directory symlinks fail closed.
5
16
  */
6
- export declare function atomicWriteText(path: string, text: string): void;
17
+ export declare function atomicWriteText(path: string, text: string, options?: AtomicWriteTextOptions): void;
7
18
  /**
8
19
  * Append one JSON audit record (mirrors `_append_audit`).
9
20
  * #2951 Phase 2: product write sink routes through containedWrite.
package/dist/cache/io.js CHANGED
@@ -3,24 +3,53 @@ import { mkdirSync, renameSync, rmSync, statSync, utimesSync } from "node:fs";
3
3
  import { tmpdir } from "node:os";
4
4
  import { basename, dirname, join, resolve } from "node:path";
5
5
  import { containedWrite } from "../fs/contained-write.js";
6
+ import { assertWriteTargetSafe } from "../fs/projection-containment.js";
6
7
  import { pythonJsonLine } from "./json.js";
7
8
  /**
8
9
  * Write text via tempfile + rename (mirrors Python `_atomic_write_text`).
9
- * #2951 Phase 2: temp payload write uses containedWrite under the parent dir.
10
+ * #2951 Phase 2: temp payload write uses containedWrite.
11
+ * #3042: when `projectRoot` is set, containment root is projectRoot (not dirname(path))
12
+ * so force-added lifecycle / cache directory symlinks fail closed.
10
13
  */
11
- export function atomicWriteText(path, text) {
12
- const dir = dirname(path);
14
+ export function atomicWriteText(path, text, options = {}) {
15
+ const targetAbs = resolve(path);
16
+ const dir = dirname(targetAbs);
17
+ const tmpBase = `${basename(targetAbs)}.${randomBytes(4).toString("hex")}.tmp`;
18
+ const tmp = join(dir, tmpBase);
19
+ if (options.projectRoot !== undefined) {
20
+ // Contain against projectRoot (parent-as-root fix #3042 / authz writeJsonContained pattern).
21
+ const root = resolve(options.projectRoot);
22
+ assertWriteTargetSafe(root, targetAbs);
23
+ try {
24
+ containedWrite({
25
+ root,
26
+ target: tmp,
27
+ data: text,
28
+ mode: "create",
29
+ });
30
+ renameSync(tmp, targetAbs);
31
+ }
32
+ catch (err) {
33
+ try {
34
+ rmSync(tmp, { force: true });
35
+ }
36
+ catch {
37
+ /* v8 ignore next -- best-effort cleanup */
38
+ }
39
+ throw err;
40
+ }
41
+ return;
42
+ }
43
+ // Legacy: parent-dir containment when no projectRoot (unit helpers / transitional call sites).
13
44
  mkdirSync(dir, { recursive: true });
14
- const tmpName = `${basename(path)}.${randomBytes(4).toString("hex")}.tmp`;
15
- const tmp = join(dir, tmpName);
16
45
  try {
17
46
  containedWrite({
18
47
  root: resolve(dir),
19
- target: tmpName,
48
+ target: tmpBase,
20
49
  data: text,
21
50
  mode: "create",
22
51
  });
23
- renameSync(tmp, path);
52
+ renameSync(tmp, targetAbs);
24
53
  }
25
54
  catch (err) {
26
55
  try {
@@ -129,14 +129,16 @@ export function cachePut(source, key, raw, options = {}) {
129
129
  incomingBytes: incomingDelta,
130
130
  });
131
131
  }
132
- atomicWriteText(join(edir, "raw.json"), rawText);
132
+ // #3042: contain cache entry writes against project root (parent of cacheRoot).
133
+ const projectRoot = dirname(resolve(cacheRoot));
134
+ atomicWriteText(join(edir, "raw.json"), rawText, { projectRoot });
133
135
  const authoritativeSize = fileSize(join(edir, "raw.json"));
134
136
  const rendered = renderContent(source, raw);
135
137
  const scanResult = scan(rendered, utcIso(clock, fetched));
136
138
  const contentPath = join(edir, "content.md");
137
139
  let contentWritten = false;
138
140
  if (scanResult.passed) {
139
- atomicWriteText(contentPath, scanResult.transformed_content);
141
+ atomicWriteText(contentPath, scanResult.transformed_content, { projectRoot });
140
142
  contentWritten = true;
141
143
  }
142
144
  else if (existsSync(contentPath)) {
@@ -158,7 +160,7 @@ export function cachePut(source, key, raw, options = {}) {
158
160
  clock,
159
161
  });
160
162
  validateMeta(meta);
161
- atomicWriteText(join(edir, "meta.json"), pythonJsonDump(meta));
163
+ atomicWriteText(join(edir, "meta.json"), pythonJsonDump(meta), { projectRoot });
162
164
  appendAudit({
163
165
  event: "cache:put",
164
166
  source,
@@ -4,6 +4,7 @@ import { VBRIEF_VERSION } from "@deftai/directive-types";
4
4
  import { evaluate as evaluateAgentsMdAdvisory } from "../agents-md-advisory/evaluate.js";
5
5
  import { contentRoot } from "../content-root.js";
6
6
  import { resolveProjectDefinitionPath } from "../layout/resolve.js";
7
+ import { DEFT_DIRECTIVE_DISABLE_FLAG_NAME, DEFT_DIRECTIVE_DISABLE_STATUS, DEFT_DIRECTIVE_DISABLE_TRACKED_WARNING, detectDeftDirectiveDisable, formatDeftDirectiveDisableMessage, isDeftDirectiveDisableActive, } from "../policy/deft-directive-disable.js";
7
8
  import { detectNoDeftDirective, NO_DEFT_DIRECTIVE_DISABLED_MESSAGE, NO_DEFT_DIRECTIVE_FLAG_NAME, NO_DEFT_DIRECTIVE_INCONSISTENT_MESSAGE, NO_DEFT_DIRECTIVE_INCONSISTENT_POLICY, } from "../policy/no-deft-directive.js";
8
9
  import { describeShadowedPlanExtension, detectShadowedPlanExtensions, } from "../policy/plan-extensions.js";
9
10
  import { loadProjectDefinition } from "../policy/resolve.js";
@@ -70,6 +71,42 @@ export function cmdDoctor(args, seams = {}) {
70
71
  const consumerContext = resolve(projectRoot) !== resolve(frameworkRoot);
71
72
  const whichFn = seams.whichFn ?? defaultWhich;
72
73
  const nowFn = seams.now ?? (() => new Date());
74
+ // #3039: temporary test kill-switch. Active (untracked) → disabled short-circuit.
75
+ // Tracked/committed flag → warn only and continue normal doctor (no enforcement bypass).
76
+ const killSwitch = detectDeftDirectiveDisable(projectRoot, { skipTrackedCache: true });
77
+ if (killSwitch.present &&
78
+ killSwitch.trackedByGit &&
79
+ !isDeftDirectiveDisableActive(projectRoot, { skipTrackedCache: true })) {
80
+ if (!jsonMode && !quietMode) {
81
+ process.stderr.write(`${DEFT_DIRECTIVE_DISABLE_TRACKED_WARNING}\n`);
82
+ }
83
+ // Continue into full doctor; do not short-circuit.
84
+ }
85
+ else if (isDeftDirectiveDisableActive(projectRoot, { skipTrackedCache: true })) {
86
+ const optOutAlso = detectNoDeftDirective(projectRoot);
87
+ const message = formatDeftDirectiveDisableMessage({
88
+ permanentOptOutAlsoPresent: optOutAlso.present,
89
+ trackedByGit: false,
90
+ });
91
+ if (jsonMode) {
92
+ const payload = {
93
+ status: DEFT_DIRECTIVE_DISABLE_STATUS,
94
+ disabled: true,
95
+ disabled_via: DEFT_DIRECTIVE_DISABLE_FLAG_NAME,
96
+ kill_switch: true,
97
+ inconsistent: false,
98
+ deposit_present: killSwitch.depositPresent,
99
+ tracked_by_git: false,
100
+ permanent_opt_out_also_present: optOutAlso.present,
101
+ message,
102
+ };
103
+ process.stdout.write(`${pythonJsonDump(payload)}\n`);
104
+ }
105
+ else if (!quietMode) {
106
+ process.stdout.write(`${message}\n`);
107
+ }
108
+ return 0;
109
+ }
73
110
  // #2926: official root opt-out — short-circuit Directive doctor when clean;
74
111
  // diagnose flag+deposit inconsistency (warn; exit dirty).
75
112
  const optOut = detectNoDeftDirective(projectRoot);
@@ -1,10 +1,16 @@
1
1
  /**
2
- * OpenClaw always-pin skill detect + doctor --fix wire (#3001).
2
+ * OpenClaw always-pin skill detect + doctor --fix wire (#3001 / #3008).
3
3
  *
4
4
  * When OpenClaw signals are present, doctor checks the main workspace skills
5
5
  * root for the four always-pin skills (#2508). Missing pins emit a warning with
6
- * remediation `deft doctor --fix`. Under fixMode, pins are symlinked (preferred)
7
- * or copied from the installed content package into the target skills dir.
6
+ * remediation `deft doctor --fix`. Under fixMode, pins are **copied** into the
7
+ * workspace skills root (not symlinked into the npm content package).
8
+ *
9
+ * OpenClaw 2026.7.x rejects workspace skills that resolve outside the configured
10
+ * skills root (`reason=symlink-escape`). A pin that is only a symlink into
11
+ * `@deftai/directive-content` therefore looks "present" on disk while the host
12
+ * never loads it (#3008). Prefer real directory copies; treat escaping
13
+ * symlinks as divergent so `--fix` can replace them.
8
14
  *
9
15
  * Multi-seat (`workspace-*`) targets only when `--openclaw-all-agents` is set.
10
16
  */
@@ -29,6 +35,7 @@ export interface OpenClawSkillPinsSeams {
29
35
  readonly symlinkDir?: (target: string, path: string) => void;
30
36
  readonly copyDir?: (src: string, dst: string) => void;
31
37
  readonly removePath?: (path: string) => void;
38
+ readonly renamePath?: (from: string, to: string) => void;
32
39
  readonly contentRootFor?: (frameworkRoot: string) => string;
33
40
  readonly isTty?: () => boolean;
34
41
  readonly readYn?: (prompt: string, defaultYes: boolean) => boolean;
@@ -77,21 +84,48 @@ export declare function listInScopeSkillsDirs(stateDir: string, allAgents: boole
77
84
  * Resolve content package skills/<id> for a pin.
78
85
  */
79
86
  export declare function resolvePinSourceDir(contentBase: string, skillId: string): string;
87
+ /**
88
+ * True when `path` is a symlink whose resolved real path is outside `skillsDir`
89
+ * (OpenClaw `symlink-escape` / #3008).
90
+ */
91
+ export declare function isEscapingSkillSymlink(skillsDir: string, path: string, seams?: Pick<OpenClawSkillPinsSeams, "lstatKind">): boolean;
80
92
  /**
81
93
  * Assess which always-pins are present / missing / divergent in a skills root.
82
94
  *
83
- * - present: directory (or symlink) with SKILL.md
95
+ * - present: real directory (or non-escaping symlink) with SKILL.md matching
96
+ * the content package when `contentBase` is supplied
84
97
  * - missing: path does not exist
85
- * - divergent: path exists but is not a usable pin (file, empty dir, broken link)
98
+ * - divergent: path exists but is not a usable OpenClaw pin (file, empty dir,
99
+ * broken link, **escaping symlink**, or **stale SKILL.md** vs package — #3008)
86
100
  */
87
- export declare function assessOpenClawPins(skillsDir: string, seams?: Pick<OpenClawSkillPinsSeams, "isDir" | "isFile" | "pathExists" | "lstatKind">): OpenClawPinAssessment;
101
+ export declare function assessOpenClawPins(skillsDir: string, seams?: Pick<OpenClawSkillPinsSeams, "isDir" | "isFile" | "pathExists" | "lstatKind">, options?: {
102
+ contentBase?: string;
103
+ }): OpenClawPinAssessment;
88
104
  /**
89
- * Install one pin into a skills root. Prefer symlink; copy on failure.
90
- * Never deletes user skills. Divergent targets require force or TTY confirm.
105
+ * Install one pin into a skills root.
106
+ *
107
+ * Default: **copy** into the workspace skills root (#3008). OpenClaw rejects
108
+ * skills that resolve outside the workspace skills root via symlink-escape, so
109
+ * symlinking into the npm content package leaves pins unloaded while doctor
110
+ * reports "present". Opt into legacy symlink-first with `preferSymlink: true`
111
+ * (still falls back to copy). Never deletes unrelated user skills. Divergent
112
+ * targets (including escaping symlinks) require force or TTY confirm.
113
+ *
114
+ * Stale copies (SKILL.md differs from package) are not "already-present" —
115
+ * doctor --fix refreshes them so upgrades land without manual delete (#3008 P1).
116
+ * Forced replace stages into a sibling temp dir first so a failed copy does not
117
+ * leave the pin permanently deleted (#3008 P1).
91
118
  */
92
119
  export declare function installOpenClawPin(skillId: OpenClawAlwaysPinSkill, sourceDir: string, skillsDir: string, options?: {
93
120
  force?: boolean;
94
121
  allowOverwrite?: boolean;
122
+ /** When true, try symlink first (legacy #3001). Default false — copy (#3008). */
123
+ preferSymlink?: boolean;
124
+ /**
125
+ * When true, refresh copied pins whose SKILL.md no longer matches the
126
+ * package (package upgrade path). Doctor --fix sets this.
127
+ */
128
+ refreshStale?: boolean;
95
129
  }, seams?: OpenClawSkillPinsSeams): PinInstallResult;
96
130
  export interface RunOpenClawSkillPinsOptions {
97
131
  readonly frameworkRoot: string;