@deftai/directive-core 0.91.0 → 0.93.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/cache/fetch.js +3 -2
- package/dist/cache/io.d.ts +13 -2
- package/dist/cache/io.js +36 -7
- package/dist/cache/operations.js +5 -3
- package/dist/doctor/index.d.ts +1 -0
- package/dist/doctor/index.js +1 -0
- package/dist/doctor/main.js +49 -0
- package/dist/doctor/openclaw-l2-adapter.d.ts +26 -0
- package/dist/doctor/openclaw-l2-adapter.js +199 -0
- package/dist/hooks/dispatcher.d.ts +6 -1
- package/dist/hooks/dispatcher.js +30 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/init-deposit/gitignore.js +3 -0
- package/dist/init-deposit/hygiene.d.ts +43 -1
- package/dist/init-deposit/hygiene.js +134 -10
- package/dist/init-deposit/index.d.ts +3 -0
- package/dist/init-deposit/index.js +3 -0
- package/dist/init-deposit/init-deposit.js +10 -0
- package/dist/init-deposit/refresh.js +11 -0
- package/dist/init-deposit/scaffold.js +6 -134
- package/dist/init-deposit/skill-discovery-deposit.d.ts +65 -0
- package/dist/init-deposit/skill-discovery-deposit.js +287 -0
- package/dist/init-deposit/skill-discovery-hosts.d.ts +94 -0
- package/dist/init-deposit/skill-discovery-hosts.js +217 -0
- package/dist/init-deposit/slash-deposit.d.ts +46 -0
- package/dist/init-deposit/slash-deposit.js +174 -0
- package/dist/orchestration/probe-session.d.ts +5 -1
- package/dist/orchestration/probe-session.js +24 -13
- package/dist/platform/agents-md.js +1 -1
- package/dist/policy/deft-directive-disable.d.ts +86 -0
- package/dist/policy/deft-directive-disable.js +167 -0
- package/dist/policy/delivery-branch.d.ts +33 -0
- package/dist/policy/delivery-branch.js +124 -0
- package/dist/policy/host-slash-commands.d.ts +28 -0
- package/dist/policy/host-slash-commands.js +103 -0
- package/dist/policy/index.d.ts +3 -0
- package/dist/policy/index.js +61 -5
- package/dist/scope/brief-io.d.ts +3 -1
- package/dist/scope/brief-io.js +5 -3
- package/dist/scope/delivery-evidence.d.ts +112 -0
- package/dist/scope/delivery-evidence.js +419 -0
- package/dist/scope/index.d.ts +1 -0
- package/dist/scope/index.js +1 -0
- package/dist/scope/main.d.ts +5 -0
- package/dist/scope/main.js +98 -3
- package/dist/scope/registry-artifact-sync.js +4 -1
- package/dist/scope/transition.d.ts +11 -1
- package/dist/scope/transition.js +30 -4
- package/dist/session/ritual-sentinel.js +21 -12
- package/dist/session/session-start-hook.d.ts +3 -0
- package/dist/session/session-start-hook.js +21 -0
- package/dist/session/session-start.js +31 -0
- package/dist/slash/emitters.d.ts +102 -0
- package/dist/slash/emitters.js +148 -0
- package/dist/slash/generator.d.ts +98 -0
- package/dist/slash/generator.js +145 -0
- package/dist/slash/index.d.ts +16 -0
- package/dist/slash/index.js +16 -0
- package/dist/slash/openclaw-adapter.d.ts +64 -0
- package/dist/slash/openclaw-adapter.js +198 -0
- package/dist/slash/openclaw-deposit.d.ts +73 -0
- package/dist/slash/openclaw-deposit.js +279 -0
- package/dist/slash/openclaw-slugs.d.ts +52 -0
- package/dist/slash/openclaw-slugs.js +126 -0
- package/dist/slash/product-set.d.ts +50 -0
- package/dist/slash/product-set.js +142 -0
- package/dist/swarm/complete-cohort.d.ts +20 -1
- package/dist/swarm/complete-cohort.js +56 -9
- package/dist/swarm/finalize-cohort-cli.js +9 -0
- package/dist/swarm/finalize-cohort.d.ts +8 -0
- package/dist/swarm/finalize-cohort.js +217 -21
- package/dist/user-config/experimental-rules.d.ts +43 -0
- package/dist/user-config/experimental-rules.js +162 -0
- package/dist/user-config/index.d.ts +1 -0
- package/dist/user-config/index.js +1 -0
- package/dist/vbrief-validate/plan-hooks.d.ts +4 -0
- package/dist/vbrief-validate/plan-hooks.js +50 -0
- package/dist/verify-source/contained-writes.js +1 -1
- package/dist/xbrief/create.d.ts +36 -0
- package/dist/xbrief/create.js +285 -0
- package/dist/xbrief/index.d.ts +14 -0
- package/dist/xbrief/index.js +42 -0
- package/dist/xbrief/paths.d.ts +37 -0
- package/dist/xbrief/paths.js +123 -0
- package/dist/xbrief/styles.d.ts +36 -0
- package/dist/xbrief/styles.js +235 -0
- package/dist/xbrief/types.d.ts +50 -0
- package/dist/xbrief/types.js +17 -0
- package/dist/xbrief/verify.d.ts +30 -0
- package/dist/xbrief/verify.js +251 -0
- package/package.json +11 -3
package/dist/scope/transition.js
CHANGED
|
@@ -8,6 +8,7 @@ import { atomicWriteBrief, formatBriefJson, readBriefForMutation } from "./brief
|
|
|
8
8
|
import { stampCompletionMetadata } from "./capacity-stamp.js";
|
|
9
9
|
import { LIFECYCLE_FOLDERS, MOVE_LABELS, STATUS_PRECONDITIONS, STAY_LABELS, TRANSITIONS, } from "./constants.js";
|
|
10
10
|
import { detectLifecycleFolder, updateDecomposedChildBackReferences, updateDecomposedParentBackReferences, } from "./decomposed-refs.js";
|
|
11
|
+
import { classifyStoredDeliveryDisposition, evaluateDeliveryGate, stampDeliveryProvenance, } from "./delivery-evidence.js";
|
|
11
12
|
import { syncProjectDefinitionAfterScopeMove } from "./project-definition-sync.js";
|
|
12
13
|
import { syncSpecificationAfterScopeMove } from "./specification-sync.js";
|
|
13
14
|
import { utcNowIso } from "./vbrief-json.js";
|
|
@@ -48,7 +49,7 @@ function stampEnvelopeUpdated(data, nowIso) {
|
|
|
48
49
|
}
|
|
49
50
|
}
|
|
50
51
|
}
|
|
51
|
-
export function runTransition(action, filePath, now = new Date()) {
|
|
52
|
+
export function runTransition(action, filePath, now = new Date(), options = {}) {
|
|
52
53
|
if (!(action in TRANSITIONS)) {
|
|
53
54
|
const valid = Object.keys(TRANSITIONS).sort().join(", ");
|
|
54
55
|
return { ok: false, message: `Unknown action '${action}'. Valid actions: ${valid}` };
|
|
@@ -95,9 +96,15 @@ export function runTransition(action, filePath, now = new Date()) {
|
|
|
95
96
|
const requiredStatus = STATUS_PRECONDITIONS[act];
|
|
96
97
|
if (requiredStatus !== undefined) {
|
|
97
98
|
if (currentStatus === targetStatus) {
|
|
99
|
+
// Surface legacy delivery disposition on already-completed briefs (#3041).
|
|
100
|
+
let dispositionSuffix = "";
|
|
101
|
+
if (act === "complete" && currentFolder === "completed") {
|
|
102
|
+
const disposition = classifyStoredDeliveryDisposition(planObj);
|
|
103
|
+
dispositionSuffix = ` (deliveryDisposition=${disposition})`;
|
|
104
|
+
}
|
|
98
105
|
return {
|
|
99
106
|
ok: true,
|
|
100
|
-
message: `No-op: ${basename} is already ${targetStatus} in ${currentFolder}
|
|
107
|
+
message: `No-op: ${basename} is already ${targetStatus} in ${currentFolder}/${dispositionSuffix}`,
|
|
101
108
|
};
|
|
102
109
|
}
|
|
103
110
|
if (currentStatus !== requiredStatus) {
|
|
@@ -131,6 +138,25 @@ export function runTransition(action, filePath, now = new Date()) {
|
|
|
131
138
|
}
|
|
132
139
|
}
|
|
133
140
|
const nowIso = utcNowIso(now);
|
|
141
|
+
// #3041: fail closed before mutating a code-bearing complete without delivery evidence.
|
|
142
|
+
if (act === "complete") {
|
|
143
|
+
const gate = evaluateDeliveryGate({
|
|
144
|
+
projectRoot,
|
|
145
|
+
plan: planObj,
|
|
146
|
+
nowIso,
|
|
147
|
+
evidence: options.deliveryEvidence,
|
|
148
|
+
nonDeliveryDisposition: options.nonDeliveryDisposition,
|
|
149
|
+
runGit: options.runGit,
|
|
150
|
+
verifier: options.verifier ?? "scope:complete",
|
|
151
|
+
assumeEvidenceValidated: options.assumeEvidenceValidated,
|
|
152
|
+
});
|
|
153
|
+
if (!gate.ok) {
|
|
154
|
+
return { ok: false, message: gate.message };
|
|
155
|
+
}
|
|
156
|
+
if (gate.provenance !== null) {
|
|
157
|
+
stampDeliveryProvenance(planObj, gate.provenance);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
134
160
|
planObj.status = targetStatus;
|
|
135
161
|
planObj.updated = nowIso;
|
|
136
162
|
// Keep the envelope clock aligned with plan.updated on every mutating transition (#2862).
|
|
@@ -153,7 +179,7 @@ export function runTransition(action, filePath, now = new Date()) {
|
|
|
153
179
|
}
|
|
154
180
|
// #2578: stamp terminal status at the destination path in the same write as
|
|
155
181
|
// folder placement — never leave a non-terminal status under completed/.
|
|
156
|
-
const writeResult = atomicWriteBrief(destPath, data, vbriefRoot);
|
|
182
|
+
const writeResult = atomicWriteBrief(destPath, data, vbriefRoot, { projectRoot });
|
|
157
183
|
if (!writeResult.ok) {
|
|
158
184
|
return { ok: false, message: writeResult.message };
|
|
159
185
|
}
|
|
@@ -193,7 +219,7 @@ export function runTransition(action, filePath, now = new Date()) {
|
|
|
193
219
|
message: `${actionLabel} ${basename}: ${currentFolder}/ -> ${targetFolder}/ (status: ${targetStatus})`,
|
|
194
220
|
};
|
|
195
221
|
}
|
|
196
|
-
const writeResult = atomicWriteBrief(resolvedPath, data, vbriefRoot);
|
|
222
|
+
const writeResult = atomicWriteBrief(resolvedPath, data, vbriefRoot, { projectRoot });
|
|
197
223
|
if (!writeResult.ok) {
|
|
198
224
|
return { ok: false, message: writeResult.message };
|
|
199
225
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { existsSync,
|
|
2
|
-
import { join, relative, resolve } from "node:path";
|
|
1
|
+
import { existsSync, readdirSync, readFileSync, renameSync, rmSync, statSync } from "node:fs";
|
|
2
|
+
import { basename, dirname, join, relative, resolve } from "node:path";
|
|
3
3
|
import { containedWrite } from "../fs/contained-write.js";
|
|
4
|
+
import { assertWriteTargetSafe } from "../fs/projection-containment.js";
|
|
4
5
|
import { hasArtifactSuffix, LEGACY_ARTIFACT_DIR, MIGRATED_ARTIFACT_DIR, } from "../layout/resolve.js";
|
|
5
6
|
import { stableJson } from "./json.js";
|
|
6
7
|
import { RITUAL_STATE_CONTRACT } from "./posture.js";
|
|
@@ -95,21 +96,29 @@ function validateSteps(raw, key) {
|
|
|
95
96
|
}
|
|
96
97
|
return [steps, null];
|
|
97
98
|
}
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
99
|
+
/**
|
|
100
|
+
* Contained atomic JSON write for ritual/sentinel state (#3042).
|
|
101
|
+
* Containment root is projectRoot (not dirname(target)) so a force-added `.deft`
|
|
102
|
+
* directory symlink fails closed before temp+rename.
|
|
103
|
+
*/
|
|
104
|
+
function atomicWriteJson(projectRoot, targetPath, payload, prefix) {
|
|
105
|
+
const root = resolve(projectRoot);
|
|
106
|
+
const abs = resolve(targetPath);
|
|
107
|
+
// Refuse leaf/parent symlinks on the final path before temp+rename publish.
|
|
108
|
+
assertWriteTargetSafe(root, abs);
|
|
109
|
+
const dir = dirname(abs);
|
|
110
|
+
const tmpBase = `${prefix}${process.pid}.${basename(abs)}.tmp`;
|
|
102
111
|
const tmpName = join(dir, tmpBase);
|
|
103
112
|
const text = `${stableJson(payload, 2)}\n`;
|
|
104
113
|
try {
|
|
105
|
-
// #2980 wave D:
|
|
114
|
+
// #2980 wave D / #3042: product write routes through containedWrite under projectRoot.
|
|
106
115
|
containedWrite({
|
|
107
|
-
root
|
|
108
|
-
target:
|
|
116
|
+
root,
|
|
117
|
+
target: tmpName,
|
|
109
118
|
data: text,
|
|
110
119
|
mode: "create",
|
|
111
120
|
});
|
|
112
|
-
renameSync(tmpName,
|
|
121
|
+
renameSync(tmpName, abs);
|
|
113
122
|
}
|
|
114
123
|
catch (err) {
|
|
115
124
|
try {
|
|
@@ -191,7 +200,7 @@ export function readRitualState(projectRoot) {
|
|
|
191
200
|
}
|
|
192
201
|
export function writeRitualState(projectRoot, payload) {
|
|
193
202
|
const stateFile = ritualStatePath(projectRoot);
|
|
194
|
-
atomicWriteJson(stateFile, payload, ".ritual-state.");
|
|
203
|
+
atomicWriteJson(projectRoot, stateFile, payload, ".ritual-state.");
|
|
195
204
|
return stateFile;
|
|
196
205
|
}
|
|
197
206
|
/** Instant guaranteed to fail `evaluateLoadedState` age checks on any policy horizon. */
|
|
@@ -303,7 +312,7 @@ export function writeSentinel(projectRoot, input) {
|
|
|
303
312
|
.replace(`${LEGACY_ARTIFACT_DIR}/active/`, `${MIGRATED_ARTIFACT_DIR}/active/`),
|
|
304
313
|
lastBranch: input.lastBranch,
|
|
305
314
|
};
|
|
306
|
-
atomicWriteJson(sentinelFile, payload, ".last-session.");
|
|
315
|
+
atomicWriteJson(projectRoot, sentinelFile, payload, ".last-session.");
|
|
307
316
|
return sentinelFile;
|
|
308
317
|
}
|
|
309
318
|
function formatElapsed(deltaMs) {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { detectDeftDirectiveDisable } from "../policy/deft-directive-disable.js";
|
|
1
2
|
import { detectNoDeftDirective } from "../policy/no-deft-directive.js";
|
|
2
3
|
import { writeSentinel } from "./ritual-sentinel.js";
|
|
3
4
|
export interface SessionStartHookOptions {
|
|
@@ -7,6 +8,8 @@ export interface SessionStartHookOptions {
|
|
|
7
8
|
readonly writeSentinelFn?: typeof writeSentinel;
|
|
8
9
|
/** Test seam for #2926 opt-out detection. */
|
|
9
10
|
readonly detectNoDeftDirectiveFn?: typeof detectNoDeftDirective;
|
|
11
|
+
/** Test seam for #3039 test kill-switch detection. */
|
|
12
|
+
readonly detectDeftDirectiveDisableFn?: typeof detectDeftDirectiveDisable;
|
|
10
13
|
}
|
|
11
14
|
/** Write ``.deft/last-session.json`` from current git state (#1269). */
|
|
12
15
|
export declare function runSessionStartHookWrite(projectRoot: string, options?: SessionStartHookOptions): {
|
|
@@ -1,9 +1,30 @@
|
|
|
1
1
|
import { resolveVersion } from "../doctor/paths.js";
|
|
2
|
+
import { detectDeftDirectiveDisable, formatDeftDirectiveDisableMessage, isDeftDirectiveDisableActive, } from "../policy/deft-directive-disable.js";
|
|
2
3
|
import { detectNoDeftDirective, NO_DEFT_DIRECTIVE_DISABLED_MESSAGE, NO_DEFT_DIRECTIVE_INCONSISTENT_MESSAGE, } from "../policy/no-deft-directive.js";
|
|
3
4
|
import { detectBranch } from "./git.js";
|
|
4
5
|
import { detectLatestActiveVbrief, writeSentinel } from "./ritual-sentinel.js";
|
|
5
6
|
/** Write ``.deft/last-session.json`` from current git state (#1269). */
|
|
6
7
|
export function runSessionStartHookWrite(projectRoot, options = {}) {
|
|
8
|
+
const detectKill = options.detectDeftDirectiveDisableFn ?? detectDeftDirectiveDisable;
|
|
9
|
+
// #3039: local (untracked) kill-switch — skip ritual bookkeeping (deposit OK).
|
|
10
|
+
// Tracked flags do not short-circuit (enforcement stays on).
|
|
11
|
+
const kill = detectKill(projectRoot);
|
|
12
|
+
const killActive = options.detectDeftDirectiveDisableFn !== undefined
|
|
13
|
+
? kill.active
|
|
14
|
+
: isDeftDirectiveDisableActive(projectRoot);
|
|
15
|
+
if (killActive) {
|
|
16
|
+
const detectOptOut = options.detectNoDeftDirectiveFn ?? detectNoDeftDirective;
|
|
17
|
+
const optOut = detectOptOut(projectRoot);
|
|
18
|
+
const message = formatDeftDirectiveDisableMessage({
|
|
19
|
+
permanentOptOutAlsoPresent: optOut.present,
|
|
20
|
+
trackedByGit: false,
|
|
21
|
+
});
|
|
22
|
+
return {
|
|
23
|
+
code: 0,
|
|
24
|
+
stdout: `${message}\n`,
|
|
25
|
+
stderr: "",
|
|
26
|
+
};
|
|
27
|
+
}
|
|
7
28
|
const detectOptOut = options.detectNoDeftDirectiveFn ?? detectNoDeftDirective;
|
|
8
29
|
// #2926: root opt-out wins — host SessionStart must not write ritual bookkeeping.
|
|
9
30
|
const optOut = detectOptOut(projectRoot);
|
|
@@ -3,6 +3,7 @@ import { runningInsideDeftRepo } from "../doctor/paths.js";
|
|
|
3
3
|
import { emitSessionEvalReadback } from "../eval/readback.js";
|
|
4
4
|
import { MIGRATE_COMPLETION_NUDGE, shouldEmitMigrateNudge } from "../init-deposit/migrate.js";
|
|
5
5
|
import { detectEnvironmentContext, environmentContextToDict, formatEnvironmentContext, } from "../platform/shell-context.js";
|
|
6
|
+
import { DEFT_DIRECTIVE_DISABLE_FLAG_NAME, DEFT_DIRECTIVE_DISABLE_STATUS, detectDeftDirectiveDisable, formatDeftDirectiveDisableMessage, isDeftDirectiveDisableActive, } from "../policy/deft-directive-disable.js";
|
|
6
7
|
import { disclosureLine } from "../policy/disclosure.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 { humanMergeDisclosureLine, resolveHumanMergePolicy, } from "../policy/require-human-merge.js";
|
|
@@ -543,6 +544,36 @@ export function runSessionStart(projectRoot, options = {}) {
|
|
|
543
544
|
const runGit = options.runGit ?? defaultGitRunner;
|
|
544
545
|
const environment = (options.probeEnvironment ?? detectEnvironmentContext)();
|
|
545
546
|
const ceremonyTier = options.ceremonyTier ?? COLD_CEREMONY_TIER;
|
|
547
|
+
// #3039: local (untracked) test kill-switch — skip ritual write; deposit may remain.
|
|
548
|
+
// Recovery requires delete + new session. Tracked flags do not short-circuit.
|
|
549
|
+
if (isDeftDirectiveDisableActive(projectRoot)) {
|
|
550
|
+
const killSwitch = detectDeftDirectiveDisable(projectRoot);
|
|
551
|
+
const optOutAlso = detectNoDeftDirective(projectRoot);
|
|
552
|
+
const message = formatDeftDirectiveDisableMessage({
|
|
553
|
+
permanentOptOutAlsoPresent: optOutAlso.present,
|
|
554
|
+
trackedByGit: false,
|
|
555
|
+
});
|
|
556
|
+
const lines = message.split("\n");
|
|
557
|
+
return {
|
|
558
|
+
code: 0,
|
|
559
|
+
payload: {
|
|
560
|
+
ready: false,
|
|
561
|
+
exit_code: 0,
|
|
562
|
+
disabled: true,
|
|
563
|
+
disabled_via: DEFT_DIRECTIVE_DISABLE_FLAG_NAME,
|
|
564
|
+
status: DEFT_DIRECTIVE_DISABLE_STATUS,
|
|
565
|
+
kill_switch: true,
|
|
566
|
+
inconsistent: false,
|
|
567
|
+
deposit_present: killSwitch.depositPresent,
|
|
568
|
+
tracked_by_git: false,
|
|
569
|
+
permanent_opt_out_also_present: optOutAlso.present,
|
|
570
|
+
posture,
|
|
571
|
+
environment: environmentContextToDict(environment),
|
|
572
|
+
message,
|
|
573
|
+
},
|
|
574
|
+
lines,
|
|
575
|
+
};
|
|
576
|
+
}
|
|
546
577
|
// #2926: official root opt-out wins locally — skip Directive session ritual.
|
|
547
578
|
// disabled = skip ritual (exit 0 clean / 1 inconsistent). ready stays false so
|
|
548
579
|
// automation does not treat opt-out as "session fully initialized for work".
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-host native command/prompt emitters (#3053 / epic #55).
|
|
3
|
+
*
|
|
4
|
+
* Maps shared thin-wrapper IR from {@link generateThinWrappers} onto each
|
|
5
|
+
* supported host’s repo-relative path layout. Pure generate-to-path records;
|
|
6
|
+
* filesystem deposit is init-deposit `writeSlashCommandDeposit` (#3054).
|
|
7
|
+
*
|
|
8
|
+
* ## Host id → output directory / file pattern
|
|
9
|
+
*
|
|
10
|
+
* | Host id | Relative directory | Filename pattern |
|
|
11
|
+
* |---------|----------------------|--------------------|
|
|
12
|
+
* | claude | `.claude/commands/` | `{hyphen-stem}.md` |
|
|
13
|
+
* | cursor | `.cursor/commands/` | `{hyphen-stem}.md` |
|
|
14
|
+
* | grok | `.grok/commands/` | `{hyphen-stem}.md` |
|
|
15
|
+
* | codex | `.codex/prompts/` | `{hyphen-stem}.md` |
|
|
16
|
+
*
|
|
17
|
+
* All four use the shared thin-wrapper markdown template (L5). Adding a host is
|
|
18
|
+
* an additive layout entry + optional frontmatter reshape — not a new product
|
|
19
|
+
* name table (L2 lives in product-set / generator only).
|
|
20
|
+
*
|
|
21
|
+
* ⊗ Empty stub dirs for “enabled but no emitter” (L6).
|
|
22
|
+
* ⊗ Native legacy alias files (L3).
|
|
23
|
+
*/
|
|
24
|
+
import { type ThinWrapperIR } from "./generator.js";
|
|
25
|
+
/**
|
|
26
|
+
* Hosts with a real slash/prompt emitter layout (aligned with hook host ids).
|
|
27
|
+
* Default enabled set for deposit (#3054) is this list when policy opts in.
|
|
28
|
+
*/
|
|
29
|
+
export declare const SLASH_EMITTER_HOSTS: readonly ["claude", "cursor", "grok", "codex"];
|
|
30
|
+
export type SlashEmitterHostId = (typeof SLASH_EMITTER_HOSTS)[number];
|
|
31
|
+
/** Repo-relative layout for one host’s native command/prompt files. */
|
|
32
|
+
export interface HostCommandLayout {
|
|
33
|
+
readonly hostId: SlashEmitterHostId;
|
|
34
|
+
/**
|
|
35
|
+
* Repo-relative directory (posix, no trailing slash).
|
|
36
|
+
* Example: `.claude/commands`
|
|
37
|
+
*/
|
|
38
|
+
readonly relativeDir: string;
|
|
39
|
+
/** Human-readable file pattern (always `{stem}.md` for v1). */
|
|
40
|
+
readonly filePattern: string;
|
|
41
|
+
/**
|
|
42
|
+
* Kind of native surface the host loads (documentation + deposit policy).
|
|
43
|
+
* Does not change file contents in v1 — all emit shared thin markdown.
|
|
44
|
+
*/
|
|
45
|
+
readonly surfaceKind: "commands" | "prompts";
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Documented host id → directory / file pattern mapping (issue #3053 AC).
|
|
49
|
+
* Frozen; additive registration only.
|
|
50
|
+
*/
|
|
51
|
+
export declare const HOST_COMMAND_LAYOUTS: Readonly<Record<SlashEmitterHostId, HostCommandLayout>>;
|
|
52
|
+
/** One host-native file ready for deposit (#3054) or tests. */
|
|
53
|
+
export interface HostEmittedFile {
|
|
54
|
+
readonly hostId: SlashEmitterHostId;
|
|
55
|
+
/** Canonical slash id from the product set. */
|
|
56
|
+
readonly logicalId: string;
|
|
57
|
+
/** Hyphen filename stem (L4). */
|
|
58
|
+
readonly filenameStem: string;
|
|
59
|
+
/** Basename including `.md`. */
|
|
60
|
+
readonly filename: string;
|
|
61
|
+
/** Repo-relative posix path, e.g. `.claude/commands/deft-continue.md`. */
|
|
62
|
+
readonly relativePath: string;
|
|
63
|
+
/** Thin-wrapper file contents (frontmatter + body). */
|
|
64
|
+
readonly contents: string;
|
|
65
|
+
readonly description: string;
|
|
66
|
+
readonly dispatchPath: string;
|
|
67
|
+
}
|
|
68
|
+
/** Type guard for {@link SlashEmitterHostId}. */
|
|
69
|
+
export declare function isSlashEmitterHostId(value: string): value is SlashEmitterHostId;
|
|
70
|
+
/** Stable list of hosts that have real emitters (no stubs). */
|
|
71
|
+
export declare function listSlashEmitterHosts(): readonly SlashEmitterHostId[];
|
|
72
|
+
/** Look up the documented layout for a host, or throw. */
|
|
73
|
+
export declare function getHostCommandLayout(hostId: SlashEmitterHostId): HostCommandLayout;
|
|
74
|
+
/**
|
|
75
|
+
* Build the repo-relative path for one IR entry under a host layout (L4 filenames).
|
|
76
|
+
*/
|
|
77
|
+
export declare function hostRelativePath(hostId: SlashEmitterHostId, filename: string): string;
|
|
78
|
+
/**
|
|
79
|
+
* Emit host-native file records for one host from shared thin-wrapper IR.
|
|
80
|
+
*
|
|
81
|
+
* Does not redefine the L2 product table — defaults to {@link generateThinWrappers}.
|
|
82
|
+
* Contents stay thin (L5); paths use L4 hyphen names under the host layout.
|
|
83
|
+
*/
|
|
84
|
+
export declare function emitHostCommandFiles(hostId: SlashEmitterHostId, wrappers?: readonly ThinWrapperIR[]): readonly HostEmittedFile[];
|
|
85
|
+
/**
|
|
86
|
+
* Emit for every host that has a real emitter (default: all of {@link SLASH_EMITTER_HOSTS}).
|
|
87
|
+
*
|
|
88
|
+
* Returns a map keyed by host id; each value has count === product set when using
|
|
89
|
+
* default IR.
|
|
90
|
+
*/
|
|
91
|
+
export declare function emitAllHostCommandFiles(hosts?: readonly SlashEmitterHostId[], wrappers?: readonly ThinWrapperIR[]): ReadonlyMap<SlashEmitterHostId, readonly HostEmittedFile[]>;
|
|
92
|
+
/**
|
|
93
|
+
* Host-specific markdown reshape hook.
|
|
94
|
+
*
|
|
95
|
+
* v1: all hosts share the generator’s host-agnostic `fileMarkdown` (description +
|
|
96
|
+
* optional argument-hint + thin body). Future hosts may remap frontmatter keys
|
|
97
|
+
* here without touching the product name table.
|
|
98
|
+
*/
|
|
99
|
+
export declare function renderHostFileContents(_hostId: SlashEmitterHostId, wrapper: ThinWrapperIR): string;
|
|
100
|
+
/** Assert emitted contents remain thin pointers (for tests and deposit validation). */
|
|
101
|
+
export declare function assertThinHostEmission(files: readonly HostEmittedFile[]): void;
|
|
102
|
+
//# sourceMappingURL=emitters.d.ts.map
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-host native command/prompt emitters (#3053 / epic #55).
|
|
3
|
+
*
|
|
4
|
+
* Maps shared thin-wrapper IR from {@link generateThinWrappers} onto each
|
|
5
|
+
* supported host’s repo-relative path layout. Pure generate-to-path records;
|
|
6
|
+
* filesystem deposit is init-deposit `writeSlashCommandDeposit` (#3054).
|
|
7
|
+
*
|
|
8
|
+
* ## Host id → output directory / file pattern
|
|
9
|
+
*
|
|
10
|
+
* | Host id | Relative directory | Filename pattern |
|
|
11
|
+
* |---------|----------------------|--------------------|
|
|
12
|
+
* | claude | `.claude/commands/` | `{hyphen-stem}.md` |
|
|
13
|
+
* | cursor | `.cursor/commands/` | `{hyphen-stem}.md` |
|
|
14
|
+
* | grok | `.grok/commands/` | `{hyphen-stem}.md` |
|
|
15
|
+
* | codex | `.codex/prompts/` | `{hyphen-stem}.md` |
|
|
16
|
+
*
|
|
17
|
+
* All four use the shared thin-wrapper markdown template (L5). Adding a host is
|
|
18
|
+
* an additive layout entry + optional frontmatter reshape — not a new product
|
|
19
|
+
* name table (L2 lives in product-set / generator only).
|
|
20
|
+
*
|
|
21
|
+
* ⊗ Empty stub dirs for “enabled but no emitter” (L6).
|
|
22
|
+
* ⊗ Native legacy alias files (L3).
|
|
23
|
+
*/
|
|
24
|
+
import { generateThinWrappers, isThinWrapperMarkdown } from "./generator.js";
|
|
25
|
+
import { PRODUCT_COMMAND_COUNT } from "./product-set.js";
|
|
26
|
+
/**
|
|
27
|
+
* Hosts with a real slash/prompt emitter layout (aligned with hook host ids).
|
|
28
|
+
* Default enabled set for deposit (#3054) is this list when policy opts in.
|
|
29
|
+
*/
|
|
30
|
+
export const SLASH_EMITTER_HOSTS = ["claude", "cursor", "grok", "codex"];
|
|
31
|
+
/**
|
|
32
|
+
* Documented host id → directory / file pattern mapping (issue #3053 AC).
|
|
33
|
+
* Frozen; additive registration only.
|
|
34
|
+
*/
|
|
35
|
+
export const HOST_COMMAND_LAYOUTS = Object.freeze({
|
|
36
|
+
claude: Object.freeze({
|
|
37
|
+
hostId: "claude",
|
|
38
|
+
relativeDir: ".claude/commands",
|
|
39
|
+
filePattern: "{stem}.md",
|
|
40
|
+
surfaceKind: "commands",
|
|
41
|
+
}),
|
|
42
|
+
cursor: Object.freeze({
|
|
43
|
+
hostId: "cursor",
|
|
44
|
+
relativeDir: ".cursor/commands",
|
|
45
|
+
filePattern: "{stem}.md",
|
|
46
|
+
surfaceKind: "commands",
|
|
47
|
+
}),
|
|
48
|
+
grok: Object.freeze({
|
|
49
|
+
hostId: "grok",
|
|
50
|
+
relativeDir: ".grok/commands",
|
|
51
|
+
filePattern: "{stem}.md",
|
|
52
|
+
surfaceKind: "commands",
|
|
53
|
+
}),
|
|
54
|
+
codex: Object.freeze({
|
|
55
|
+
hostId: "codex",
|
|
56
|
+
relativeDir: ".codex/prompts",
|
|
57
|
+
filePattern: "{stem}.md",
|
|
58
|
+
surfaceKind: "prompts",
|
|
59
|
+
}),
|
|
60
|
+
});
|
|
61
|
+
/** Type guard for {@link SlashEmitterHostId}. */
|
|
62
|
+
export function isSlashEmitterHostId(value) {
|
|
63
|
+
return SLASH_EMITTER_HOSTS.includes(value);
|
|
64
|
+
}
|
|
65
|
+
/** Stable list of hosts that have real emitters (no stubs). */
|
|
66
|
+
export function listSlashEmitterHosts() {
|
|
67
|
+
return SLASH_EMITTER_HOSTS;
|
|
68
|
+
}
|
|
69
|
+
/** Look up the documented layout for a host, or throw. */
|
|
70
|
+
export function getHostCommandLayout(hostId) {
|
|
71
|
+
const layout = HOST_COMMAND_LAYOUTS[hostId];
|
|
72
|
+
if (layout === undefined) {
|
|
73
|
+
throw new Error(`No slash emitter layout for host: ${hostId}`);
|
|
74
|
+
}
|
|
75
|
+
return layout;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Build the repo-relative path for one IR entry under a host layout (L4 filenames).
|
|
79
|
+
*/
|
|
80
|
+
export function hostRelativePath(hostId, filename) {
|
|
81
|
+
const layout = getHostCommandLayout(hostId);
|
|
82
|
+
// Defensive: refuse path separators in filename so deposit cannot escape relativeDir.
|
|
83
|
+
if (filename.includes("/") || filename.includes("\\") || filename.includes("..")) {
|
|
84
|
+
throw new Error(`Invalid command filename for host emit: ${filename}`);
|
|
85
|
+
}
|
|
86
|
+
return `${layout.relativeDir}/${filename}`;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Emit host-native file records for one host from shared thin-wrapper IR.
|
|
90
|
+
*
|
|
91
|
+
* Does not redefine the L2 product table — defaults to {@link generateThinWrappers}.
|
|
92
|
+
* Contents stay thin (L5); paths use L4 hyphen names under the host layout.
|
|
93
|
+
*/
|
|
94
|
+
export function emitHostCommandFiles(hostId, wrappers = generateThinWrappers()) {
|
|
95
|
+
if (!isSlashEmitterHostId(hostId)) {
|
|
96
|
+
throw new Error(`Unknown slash emitter host: ${String(hostId)}`);
|
|
97
|
+
}
|
|
98
|
+
// Snapshot layout once; additive hosts must register before emit.
|
|
99
|
+
getHostCommandLayout(hostId);
|
|
100
|
+
return wrappers.map((w) => {
|
|
101
|
+
const contents = renderHostFileContents(hostId, w);
|
|
102
|
+
return {
|
|
103
|
+
hostId,
|
|
104
|
+
logicalId: w.logicalId,
|
|
105
|
+
filenameStem: w.filenameStem,
|
|
106
|
+
filename: w.filename,
|
|
107
|
+
relativePath: hostRelativePath(hostId, w.filename),
|
|
108
|
+
contents,
|
|
109
|
+
description: w.description,
|
|
110
|
+
dispatchPath: w.dispatchPath,
|
|
111
|
+
};
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Emit for every host that has a real emitter (default: all of {@link SLASH_EMITTER_HOSTS}).
|
|
116
|
+
*
|
|
117
|
+
* Returns a map keyed by host id; each value has count === product set when using
|
|
118
|
+
* default IR.
|
|
119
|
+
*/
|
|
120
|
+
export function emitAllHostCommandFiles(hosts = SLASH_EMITTER_HOSTS, wrappers = generateThinWrappers()) {
|
|
121
|
+
const out = new Map();
|
|
122
|
+
for (const hostId of hosts) {
|
|
123
|
+
out.set(hostId, emitHostCommandFiles(hostId, wrappers));
|
|
124
|
+
}
|
|
125
|
+
return out;
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Host-specific markdown reshape hook.
|
|
129
|
+
*
|
|
130
|
+
* v1: all hosts share the generator’s host-agnostic `fileMarkdown` (description +
|
|
131
|
+
* optional argument-hint + thin body). Future hosts may remap frontmatter keys
|
|
132
|
+
* here without touching the product name table.
|
|
133
|
+
*/
|
|
134
|
+
export function renderHostFileContents(_hostId, wrapper) {
|
|
135
|
+
return wrapper.fileMarkdown;
|
|
136
|
+
}
|
|
137
|
+
/** Assert emitted contents remain thin pointers (for tests and deposit validation). */
|
|
138
|
+
export function assertThinHostEmission(files) {
|
|
139
|
+
if (files.length !== PRODUCT_COMMAND_COUNT) {
|
|
140
|
+
throw new Error(`Expected ${PRODUCT_COMMAND_COUNT} host command files, got ${files.length}`);
|
|
141
|
+
}
|
|
142
|
+
for (const f of files) {
|
|
143
|
+
if (!isThinWrapperMarkdown(f.contents, f.dispatchPath)) {
|
|
144
|
+
throw new Error(`Non-thin emission for ${f.relativePath}`);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
//# sourceMappingURL=emitters.js.map
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Host-agnostic thin-wrapper generator for product slash commands (#3052 / epic #55).
|
|
3
|
+
*
|
|
4
|
+
* Emits IR + markdown templates that per-host emitters (#3053) format into native
|
|
5
|
+
* command/prompt/workflow files. Wrappers stay thin (L5): frontmatter description +
|
|
6
|
+
* short dispatch pointer — never inlined strategy/skill/commands.md bodies.
|
|
7
|
+
*
|
|
8
|
+
* ## Token / context budgets (from #55 token design rules)
|
|
9
|
+
*
|
|
10
|
+
* | When | Target |
|
|
11
|
+
* |---|---|
|
|
12
|
+
* | Idle (user never invokes `/deft…`) | ~0 from command files |
|
|
13
|
+
* | Catalog (`/` menu: name + description × N) | ≤ ~1k tok for the product set |
|
|
14
|
+
* | Single invoke | ~40–100 tok thin wrapper body |
|
|
15
|
+
* | After dispatch | cost of the target strategy/skill (unchanged) |
|
|
16
|
+
*
|
|
17
|
+
* Multi-host deposit does not multiply tokens in one session: each host reads only
|
|
18
|
+
* its own command directory. Real spend is the loaded artifact after invoke.
|
|
19
|
+
*/
|
|
20
|
+
import { logicalIdToFilename, logicalIdToFilenameStem, PRODUCT_COMMAND_COUNT, type ProductCommand } from "./product-set.js";
|
|
21
|
+
/** Rough UTF-8 bytes-per-token estimate (aligned with agents-md-budget). */
|
|
22
|
+
export declare const BYTES_PER_TOKEN_ESTIMATE = 4;
|
|
23
|
+
/** L5: invoke wrapper body budget (≈100 tokens). */
|
|
24
|
+
export declare const MAX_WRAPPER_BODY_TOKENS = 100;
|
|
25
|
+
/** Per-command catalog description hard cap for tests (order-of-magnitude 20–50). */
|
|
26
|
+
export declare const MAX_DESCRIPTION_TOKENS = 50;
|
|
27
|
+
/** Full product-set description catalog budget (≤ ~1k tok). */
|
|
28
|
+
export declare const MAX_CATALOG_TOKENS = 1000;
|
|
29
|
+
/**
|
|
30
|
+
* Host-agnostic intermediate representation for one thin wrapper.
|
|
31
|
+
*
|
|
32
|
+
* Emitters consume this shape without re-listing product names.
|
|
33
|
+
*/
|
|
34
|
+
export interface ThinWrapperIR {
|
|
35
|
+
/** Canonical slash id, e.g. `/deft:directive:run:interview`. */
|
|
36
|
+
readonly logicalId: string;
|
|
37
|
+
/** Hyphen stem without extension. */
|
|
38
|
+
readonly filenameStem: string;
|
|
39
|
+
/** On-disk filename including `.md`. */
|
|
40
|
+
readonly filename: string;
|
|
41
|
+
/** Catalog description (host frontmatter `description`). */
|
|
42
|
+
readonly description: string;
|
|
43
|
+
readonly dispatchKind: ProductCommand["dispatchKind"];
|
|
44
|
+
/** Content-root-relative primary load path. */
|
|
45
|
+
readonly dispatchPath: string;
|
|
46
|
+
readonly argumentHint?: string;
|
|
47
|
+
/** Body markdown only (no frontmatter). */
|
|
48
|
+
readonly bodyMarkdown: string;
|
|
49
|
+
/** Full file: YAML frontmatter + body (host-agnostic template). */
|
|
50
|
+
readonly fileMarkdown: string;
|
|
51
|
+
/** Estimated body tokens (UTF-8 bytes / {@link BYTES_PER_TOKEN_ESTIMATE}). */
|
|
52
|
+
readonly estimatedBodyTokens: number;
|
|
53
|
+
/** Estimated description tokens. */
|
|
54
|
+
readonly estimatedDescriptionTokens: number;
|
|
55
|
+
}
|
|
56
|
+
/** Aggregate token budget report for the product set. */
|
|
57
|
+
export interface TokenBudgetReport {
|
|
58
|
+
readonly commandCount: number;
|
|
59
|
+
readonly catalogTokens: number;
|
|
60
|
+
readonly maxBodyTokens: number;
|
|
61
|
+
readonly maxDescriptionTokens: number;
|
|
62
|
+
readonly withinBodyBudget: boolean;
|
|
63
|
+
readonly withinDescriptionBudget: boolean;
|
|
64
|
+
readonly withinCatalogBudget: boolean;
|
|
65
|
+
readonly ok: boolean;
|
|
66
|
+
}
|
|
67
|
+
/** Estimate tokens from a UTF-8 string (bytes / 4). */
|
|
68
|
+
export declare function estimateTokens(text: string): number;
|
|
69
|
+
/**
|
|
70
|
+
* Render the thin body only: short dispatch pointer, no inlined target content.
|
|
71
|
+
*
|
|
72
|
+
* Keeps invoke cost in the ~40–100 token band (L5).
|
|
73
|
+
*/
|
|
74
|
+
export declare function renderThinWrapperBody(command: ProductCommand): string;
|
|
75
|
+
/**
|
|
76
|
+
* Render host-agnostic file markdown: description frontmatter + thin body.
|
|
77
|
+
*
|
|
78
|
+
* Per-host emitters may re-shape frontmatter keys; the body semantics stay shared.
|
|
79
|
+
*/
|
|
80
|
+
export declare function renderThinWrapperFile(command: ProductCommand): string;
|
|
81
|
+
/** Build {@link ThinWrapperIR} for one product command. */
|
|
82
|
+
export declare function generateThinWrapper(command: ProductCommand): ThinWrapperIR;
|
|
83
|
+
/**
|
|
84
|
+
* Generate thin wrappers for the full L2 product set (stable order, count === 13).
|
|
85
|
+
*
|
|
86
|
+
* This is the primary API for #3053 emitters.
|
|
87
|
+
*/
|
|
88
|
+
export declare function generateThinWrappers(commands?: readonly ProductCommand[]): readonly ThinWrapperIR[];
|
|
89
|
+
/** Measure catalog + per-wrapper body budgets for the generated set. */
|
|
90
|
+
export declare function measureTokenBudget(wrappers?: readonly ThinWrapperIR[]): TokenBudgetReport;
|
|
91
|
+
/**
|
|
92
|
+
* Structural check that a wrapper file looks like a thin pointer template.
|
|
93
|
+
* Used by unit tests and available to emitters for deposit validation.
|
|
94
|
+
*/
|
|
95
|
+
export declare function isThinWrapperMarkdown(fileMarkdown: string, dispatchPath: string): boolean;
|
|
96
|
+
/** Re-export mapping helpers for emitter convenience without a second import path. */
|
|
97
|
+
export { logicalIdToFilename, logicalIdToFilenameStem, PRODUCT_COMMAND_COUNT };
|
|
98
|
+
//# sourceMappingURL=generator.d.ts.map
|