@outcomeeng/spx 0.6.19 → 0.6.20

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/README.md CHANGED
@@ -2,6 +2,8 @@
2
2
 
3
3
  Developer CLI for code validation and session management.
4
4
 
5
+ Current release: 0.6.20
6
+
5
7
  ## What is spx?
6
8
 
7
9
  `spx` is a command-line interface (CLI) tool that provides code validation and session management for projects that implement the spec-as-source methodology named [Outcome Engineering](https://outcome.engineering). The `spx` CLI works hand-in-hand with the **Claude Code** and **Codex** [plugin marketplace for Outcome Engineering](https://github.com/outcomeeng/plugins).
@@ -103,6 +105,18 @@ Sessions are stored in `.spx/sessions/` with priority-based ordering (high > med
103
105
 
104
106
  The `spx spec` CLI provides deterministic inspection and evidence-projection commands such as `status`, `next`, and `context`. Spec authoring and tree-management workflows live in the **spec-tree** Claude Code plugin, available at [`outcomeeng/plugins`](https://github.com/outcomeeng/plugins). The plugin provides skills for understanding, authoring, decomposing, contextualizing, testing, refactoring, and aligning specification trees.
105
107
 
108
+ ### Release Preparation
109
+
110
+ Prepare release artifacts after updating the package version:
111
+
112
+ ```bash
113
+ # Generate release notes from product history
114
+ spx release notes
115
+
116
+ # Update configured release documentation for the current package version
117
+ spx release docs sync
118
+ ```
119
+
106
120
  ## Development
107
121
 
108
122
  ### Setup
package/dist/cli.js CHANGED
@@ -10055,12 +10055,17 @@ var AGENT_RUN_TOOLS = {
10055
10055
  WRITE: "Write",
10056
10056
  EDIT: "Edit"
10057
10057
  };
10058
+ var AGENT_FILE_TOOLS = [
10059
+ AGENT_RUN_TOOLS.READ,
10060
+ AGENT_RUN_TOOLS.WRITE,
10061
+ AGENT_RUN_TOOLS.EDIT
10062
+ ];
10058
10063
  var AGENT_TOOL_PERMISSION_BEHAVIOR = {
10059
10064
  ALLOW: "allow",
10060
10065
  DENY: "deny"
10061
10066
  };
10062
10067
  function authorizeAgentFileToolPath(workingDirectory, tool, filePath) {
10063
- return (tool === AGENT_RUN_TOOLS.WRITE || tool === AGENT_RUN_TOOLS.EDIT) && filePath.length > 0 && isPathContained(workingDirectory, filePath) ? AGENT_TOOL_PERMISSION_BEHAVIOR.ALLOW : AGENT_TOOL_PERMISSION_BEHAVIOR.DENY;
10068
+ return AGENT_FILE_TOOLS.includes(tool) && filePath.length > 0 && isPathContained(workingDirectory, filePath) ? AGENT_TOOL_PERMISSION_BEHAVIOR.ALLOW : AGENT_TOOL_PERMISSION_BEHAVIOR.DENY;
10064
10069
  }
10065
10070
  var AGENT_PERMISSION_MODES = {
10066
10071
  DONT_ASK: "dontAsk"
@@ -10068,6 +10073,7 @@ var AGENT_PERMISSION_MODES = {
10068
10073
 
10069
10074
  // src/agent/claude-agent-runner.ts
10070
10075
  var AGENT_FILE_TOOL_PATH_INPUT_FIELD = "file_path";
10076
+ var AGENT_PRE_TOOL_USE_HOOK_EVENT = "PreToolUse";
10071
10077
  var AGENT_FILE_TOOL_PERMISSION_DENIED_MESSAGE = "Agent file tool target is outside its working directory";
10072
10078
  var ClaudeAgentRunner = class {
10073
10079
  async run(request) {
@@ -10096,41 +10102,54 @@ function createAgentRunOptions(request) {
10096
10102
  cwd: request.workingDirectory,
10097
10103
  settingSources: [],
10098
10104
  tools: [...request.tools],
10099
- allowedTools: autoAllowedAgentTools(request.allowedTools),
10100
- canUseTool: createAgentToolPermission(request),
10105
+ allowedTools: [...request.allowedTools],
10106
+ hooks: {
10107
+ PreToolUse: [{ hooks: [createAgentFileToolPermissionHook(request)] }]
10108
+ },
10101
10109
  permissionMode: request.permissionMode,
10102
10110
  maxTurns: request.maxTurns
10103
10111
  };
10104
10112
  }
10105
- function autoAllowedAgentTools(allowedTools) {
10106
- return allowedTools.filter((tool) => !isAgentFileMutationTool(tool));
10107
- }
10108
- function createAgentToolPermission(request) {
10109
- return async (toolName, input) => {
10110
- if (!isAgentRunTool(toolName) || !request.allowedTools.includes(toolName)) {
10111
- return deniedAgentToolPermission();
10113
+ function createAgentFileToolPermissionHook(request) {
10114
+ return async (input) => {
10115
+ if (input.hook_event_name !== AGENT_PRE_TOOL_USE_HOOK_EVENT || !isAgentRunTool(input.tool_name) || !request.allowedTools.includes(input.tool_name) || !isRecord8(input.tool_input)) {
10116
+ return deniedAgentFileToolPermission();
10112
10117
  }
10113
- if (!isAgentFileMutationTool(toolName)) {
10114
- return { behavior: AGENT_TOOL_PERMISSION_BEHAVIOR.ALLOW, updatedInput: input };
10118
+ if (!isAgentFileTool(input.tool_name)) {
10119
+ return allowedAgentFileToolPermission();
10115
10120
  }
10116
- const filePath = input[AGENT_FILE_TOOL_PATH_INPUT_FIELD];
10117
- if (typeof filePath !== "string" || authorizeAgentFileToolPath(request.workingDirectory, toolName, filePath) === AGENT_TOOL_PERMISSION_BEHAVIOR.DENY) {
10118
- return deniedAgentToolPermission();
10121
+ const filePath = input.tool_input[AGENT_FILE_TOOL_PATH_INPUT_FIELD];
10122
+ if (typeof filePath !== "string" || authorizeAgentFileToolPath(request.workingDirectory, input.tool_name, filePath) === AGENT_TOOL_PERMISSION_BEHAVIOR.DENY) {
10123
+ return deniedAgentFileToolPermission();
10119
10124
  }
10120
- return { behavior: AGENT_TOOL_PERMISSION_BEHAVIOR.ALLOW, updatedInput: input };
10125
+ return allowedAgentFileToolPermission();
10121
10126
  };
10122
10127
  }
10123
- function deniedAgentToolPermission() {
10128
+ function allowedAgentFileToolPermission() {
10124
10129
  return {
10125
- behavior: AGENT_TOOL_PERMISSION_BEHAVIOR.DENY,
10126
- message: AGENT_FILE_TOOL_PERMISSION_DENIED_MESSAGE
10130
+ hookSpecificOutput: {
10131
+ hookEventName: AGENT_PRE_TOOL_USE_HOOK_EVENT,
10132
+ permissionDecision: AGENT_TOOL_PERMISSION_BEHAVIOR.ALLOW
10133
+ }
10127
10134
  };
10128
10135
  }
10136
+ function deniedAgentFileToolPermission() {
10137
+ return {
10138
+ hookSpecificOutput: {
10139
+ hookEventName: AGENT_PRE_TOOL_USE_HOOK_EVENT,
10140
+ permissionDecision: AGENT_TOOL_PERMISSION_BEHAVIOR.DENY,
10141
+ permissionDecisionReason: AGENT_FILE_TOOL_PERMISSION_DENIED_MESSAGE
10142
+ }
10143
+ };
10144
+ }
10145
+ function isRecord8(input) {
10146
+ return typeof input === "object" && input !== null;
10147
+ }
10129
10148
  function isAgentRunTool(toolName) {
10130
10149
  return Object.values(AGENT_RUN_TOOLS).some((tool) => tool === toolName);
10131
10150
  }
10132
- function isAgentFileMutationTool(tool) {
10133
- return tool === AGENT_RUN_TOOLS.WRITE || tool === AGENT_RUN_TOOLS.EDIT;
10151
+ function isAgentFileTool(tool) {
10152
+ return AGENT_FILE_TOOLS.includes(tool);
10134
10153
  }
10135
10154
  async function runClaudeQuery(prompt, options) {
10136
10155
  let result;
@@ -10302,7 +10321,9 @@ function toComponent(value) {
10302
10321
  }
10303
10322
 
10304
10323
  // src/domains/release/documentation-sync.ts
10305
- var DOCUMENTATION_SYNC_PROMPT_INSTRUCTION = "Update every staged documentation file so it contains the released version and its version references and behavior descriptions match the supplied release data, including when no previous release exists.";
10324
+ var DOCUMENTATION_SYNC_PROMPT_INSTRUCTION = "Edit every staged documentation file so its version references and behavior descriptions match the supplied release data.";
10325
+ var DOCUMENTATION_SYNC_RELEASE_VERSION_INSTRUCTION = "The exact released version every staged document must contain is";
10326
+ var DOCUMENTATION_SYNC_VERSIONLESS_INSTRUCTION = "Replace every standalone previous product release-version reference. When a staged document has no such reference, add a concise current-release reference using the exact released version above.";
10306
10327
  var DOCUMENTATION_SYNC_PROMPT_DATA_BLOCK_OPEN = "<documentation-sync-input>";
10307
10328
  var DOCUMENTATION_SYNC_PROMPT_DATA_BLOCK_CLOSE = "</documentation-sync-input>";
10308
10329
  var DOCUMENTATION_SYNC_AGENT_TOOLS = [
@@ -10315,11 +10336,15 @@ var DOCUMENTATION_SYNC_AGENT_MAX_TURNS = 10;
10315
10336
  var DOCUMENTATION_SYNC_AUDIT_MAX_TURNS = 3;
10316
10337
  var DOCUMENTATION_SYNC_AUDIT_APPROVED = "APPROVED";
10317
10338
  var DOCUMENTATION_SYNC_AUDIT_REJECTED = "REJECTED";
10339
+ var DOCUMENTATION_SYNC_AUDIT_VERSIONLESS_INSTRUCTION = "When an original document has no previous-release reference, adding a concise current-release reference using the exact released version is a supported release update.";
10318
10340
  var REGEXP_SPECIAL_CHARACTER_PATTERN = /[.*+?^${}()|[\]\\]/gu;
10319
10341
  var REGEXP_ESCAPE_REPLACEMENT = String.raw`\$&`;
10320
10342
  var VERSION_REFERENCE_NON_WHITESPACE_PATTERN = String.raw`\S`;
10321
10343
  function buildDocumentationSyncPrompt(input) {
10344
+ const encodedVersion = encodeReleasePromptData(input.releaseData.version).slice(1, -1);
10322
10345
  return `${DOCUMENTATION_SYNC_PROMPT_INSTRUCTION}
10346
+ ${DOCUMENTATION_SYNC_RELEASE_VERSION_INSTRUCTION} ${encodedVersion}.
10347
+ ${DOCUMENTATION_SYNC_VERSIONLESS_INSTRUCTION}
10323
10348
 
10324
10349
  ${DOCUMENTATION_SYNC_PROMPT_DATA_BLOCK_OPEN}
10325
10350
  ${encodeReleasePromptData(input)}
@@ -10407,6 +10432,7 @@ function containsReleaseVersionReference(content, version2) {
10407
10432
  function buildDocumentationFaithfulnessAuditPrompt(input) {
10408
10433
  return [
10409
10434
  "Audit whether every original-to-updated documentation transformation faithfully applies the supplied release data, including updating each previous-release reference rather than deleting it.",
10435
+ DOCUMENTATION_SYNC_AUDIT_VERSIONLESS_INSTRUCTION,
10410
10436
  `Return exactly ${DOCUMENTATION_SYNC_AUDIT_APPROVED} when every changed claim is supported and every previous-release reference remains represented by the released version.`,
10411
10437
  `Return ${DOCUMENTATION_SYNC_AUDIT_REJECTED} followed by a concise reason for any unsupported claim, deleted previous-release reference, or omitted release update.`,
10412
10438
  DOCUMENTATION_SYNC_PROMPT_DATA_BLOCK_OPEN,
@@ -16084,7 +16110,7 @@ async function readTestRunStatePath(runFilePath, fs4) {
16084
16110
  return { ok: true, value: validated.value };
16085
16111
  }
16086
16112
  function validateTestRunState(value) {
16087
- if (!isRecord8(value)) return { ok: false, error: "testing run state must be an object" };
16113
+ if (!isRecord9(value)) return { ok: false, error: "testing run state must be an object" };
16088
16114
  const branchName = readString2(value, TEST_RUN_STATE_FIELDS.BRANCH_NAME);
16089
16115
  if (!branchName.ok) return branchName;
16090
16116
  const headSha = readString2(value, TEST_RUN_STATE_FIELDS.HEAD_SHA);
@@ -16127,7 +16153,7 @@ function readRunnerOutcomes(raw) {
16127
16153
  }
16128
16154
  const outcomes = [];
16129
16155
  for (const entry of raw) {
16130
- if (!isRecord8(entry)) {
16156
+ if (!isRecord9(entry)) {
16131
16157
  return { ok: false, error: `${TEST_RUN_STATE_FIELDS.RUNNER_OUTCOMES} entries must be objects` };
16132
16158
  }
16133
16159
  const runnerId = readString2(entry, TEST_RUNNER_OUTCOME_FIELDS.RUNNER_ID);
@@ -16148,7 +16174,7 @@ function readProductInputDigests(raw) {
16148
16174
  }
16149
16175
  const digests = [];
16150
16176
  for (const entry of raw) {
16151
- if (!isRecord8(entry)) {
16177
+ if (!isRecord9(entry)) {
16152
16178
  return { ok: false, error: `${TEST_RUN_STATE_FIELDS.PRODUCT_INPUT_DIGESTS} entries must be objects` };
16153
16179
  }
16154
16180
  const descriptorId = readString2(entry, PRODUCT_INPUT_DIGEST_FIELDS.DESCRIPTOR_ID);
@@ -16210,7 +16236,7 @@ function testingWriteError(error) {
16210
16236
  function withDomainErrorDetail(domainError, detail) {
16211
16237
  return detail === void 0 ? domainError : `${domainError}: ${detail}`;
16212
16238
  }
16213
- function isRecord8(value) {
16239
+ function isRecord9(value) {
16214
16240
  return typeof value === "object" && value !== null && !Array.isArray(value);
16215
16241
  }
16216
16242
  function sha256Hex2(value) {
@@ -17349,7 +17375,7 @@ function importSpecifiers(sourceText, testPath) {
17349
17375
  }
17350
17376
  return specifiers;
17351
17377
  }
17352
- function isRecord9(value) {
17378
+ function isRecord10(value) {
17353
17379
  return typeof value === "object" && value !== null && !Array.isArray(value);
17354
17380
  }
17355
17381
  function hasErrorCode3(error, code) {
@@ -17384,11 +17410,11 @@ function pathMappingFromTsconfigPath(pathAlias, targets) {
17384
17410
  };
17385
17411
  }
17386
17412
  function pathMappingsFromTsconfig(config) {
17387
- if (!isRecord9(config)) return [];
17413
+ if (!isRecord10(config)) return [];
17388
17414
  const compilerOptions = config[TSCONFIG_COMPILER_OPTIONS_KEY];
17389
- if (!isRecord9(compilerOptions)) return [];
17415
+ if (!isRecord10(compilerOptions)) return [];
17390
17416
  const paths = compilerOptions[TSCONFIG_PATHS_KEY];
17391
- if (!isRecord9(paths)) return [];
17417
+ if (!isRecord10(paths)) return [];
17392
17418
  return Object.entries(paths).flatMap(([pathAlias, targets]) => {
17393
17419
  const mapping = pathMappingFromTsconfigPath(pathAlias, targets);
17394
17420
  return mapping === null ? [] : [mapping];