agent-policy-kit 0.4.0 → 0.5.1

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/src/explicit.mjs CHANGED
@@ -11,7 +11,8 @@ function explicitList(value,limit){
11
11
  return list;
12
12
  }
13
13
 
14
- // Grok has no per-role effort, so its value is the whole model id; a trailing ':<effort>' would be ambiguous and is refused.
14
+ // Grok accepts 'role=model' (native default, as in 0.4.0), 'role=model:<effort>' and 'role=model:native'.
15
+ // A model id may contain colons, so only a trailing effort name or 'native' is read as the effort.
15
16
  const grokEffortSuffix=model=>{const colon=model.lastIndexOf(':');return colon>=0&&['native',...EXPLICIT_EFFORTS].includes(model.slice(colon+1));};
16
17
  export function parseExplicitOptions(options,roleIds){
17
18
  if(!options||typeof options!=='object'||Array.isArray(options))invalidExplicit();
@@ -32,8 +33,8 @@ export function parseExplicitOptions(options,roleIds){
32
33
  if(at<1||!roles.includes(role)||Object.hasOwn(choices,role))invalidExplicit();
33
34
  let model=value,effort=null;
34
35
  if(client==='gemini'){if(!['pro','flash','flash_lite','inherit'].includes(model))invalidExplicit();}
35
- else if(client==='grok'){if(grokEffortSuffix(value))invalidExplicit();}
36
- else{
36
+ // A Grok value without an effort suffix is the whole model id.
37
+ else if(client!=='grok'||grokEffortSuffix(value)){
37
38
  const colon=value.lastIndexOf(':');if(colon<1)invalidExplicit();
38
39
  model=value.slice(0,colon);const level=value.slice(colon+1);
39
40
  if(level!=='native'&&!EXPLICIT_EFFORTS.includes(level))invalidExplicit();effort=level==='native'?null:level;
@@ -68,9 +69,10 @@ export function explicitOptions(config,roleCatalog){
68
69
  options[flag]=roles.map(role=>{
69
70
  const selected=preferences.roles?.[role]??preferences[kinds.get(role)];
70
71
  if(!selected||typeof selected.model!=='string'||!EXPLICIT_MODEL.test(selected.model)||(selected.effort!==null&&!EXPLICIT_EFFORTS.includes(selected.effort)))invalidExplicit();
71
- if(['gemini','grok'].includes(client)&&selected.effort!==null)invalidExplicit();
72
- if(client==='grok'&&grokEffortSuffix(selected.model))invalidExplicit();
73
- return `${role}=${selected.model}${['gemini','grok'].includes(client)?'':':'+(selected.effort??'native')}`;
72
+ if(client==='gemini'&&selected.effort!==null)invalidExplicit();
73
+ // A Grok native default keeps the 0.4.0 form unless the model id itself ends like an effort suffix.
74
+ const plain=client==='gemini'||(client==='grok'&&selected.effort===null&&!grokEffortSuffix(selected.model));
75
+ return `${role}=${selected.model}${plain?'':':'+(selected.effort??'native')}`;
74
76
  }).join(',');
75
77
  if(preferences.maxConcurrent!==undefined)options[flag+'-max-concurrent']=String(preferences.maxConcurrent);
76
78
  if(!Array.isArray(preferences.avoidModels)||preferences.avoidModels.some(model=>typeof model!=='string'||!EXPLICIT_MODEL.test(model)))invalidExplicit();
package/src/inspect.mjs CHANGED
@@ -2,7 +2,7 @@ import fs from 'node:fs/promises';
2
2
  import {constants} from 'node:fs';
3
3
  import path from 'node:path';
4
4
  import {parseToml} from './toml.mjs';
5
- import {CLIENTS} from './policy.mjs';
5
+ import {CLIENTS,NATIVE_EFFORTS} from './policy.mjs';
6
6
  import {inspectConfiguration} from './install.mjs';
7
7
 
8
8
  const ROOTS={chatgpt:'.codex',claude:'.claude',kimi:'.kimi-code',gemini:'.gemini',grok:'.grok'};
@@ -43,7 +43,7 @@ async function readFile(file,budget){
43
43
  finally{await handle?.close();}
44
44
  }
45
45
  function warning(client,error){
46
- const allowed=['UNSAFE_FILE','INSPECTION_LIMIT','CONFIG_CHANGED_DURING_READ','UNSUPPORTED_NATIVE_SETTING','UNSUPPORTED_ROLE_FORMAT','UNSUPPORTED_ROLE_EFFORT','INVALID_TOML','INVALID_JSON','EACCES','EPERM','NATIVE_CAP_CONFLICT','EXTERNAL_ROLE_REFERENCES_NOT_READ'];
46
+ const allowed=['UNSAFE_FILE','INSPECTION_LIMIT','CONFIG_CHANGED_DURING_READ','UNSUPPORTED_NATIVE_SETTING','UNSUPPORTED_ROLE_FORMAT','UNSUPPORTED_ROLE_EFFORT','UNSUPPORTED_ROLE_CAPABILITY','INVALID_TOML','INVALID_JSON','EACCES','EPERM','NATIVE_CAP_CONFLICT','EXTERNAL_ROLE_REFERENCES_NOT_READ'];
47
47
  const code=allowed.includes(error?.code)?error.code:'NATIVE_READ_FAILED';
48
48
  if(!client.warnings.includes(code))client.warnings.push(code);
49
49
  client.status='attention';
@@ -56,7 +56,9 @@ function scalar(value){
56
56
  if(!match)throw failure('UNSUPPORTED_ROLE_FORMAT');
57
57
  return match[1];
58
58
  }
59
- function frontmatter(text){
59
+ // Values that Grok Build 1.0.70 accepts for capabilityMode (serde kebab-case names and aliases of SubagentCapabilityMode).
60
+ const GROK_CAPABILITY_MODES=['read-only','readonly','readOnly','read_only','ReadOnly','read-write','readwrite','readWrite','read_write','ReadWrite','execute','Execute','EXECUTE','all','All','ALL'];
61
+ function frontmatter(text,id){
60
62
  const match=text.replace(/^\uFEFF/,'').match(/^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/);
61
63
  if(!match)throw failure('UNSUPPORTED_ROLE_FORMAT');
62
64
  const fields={};
@@ -64,13 +66,17 @@ function frontmatter(text){
64
66
  // YAML merges, quoted keys and flow mappings can supply model fields that
65
67
  // this deliberately small scalar reader cannot resolve safely.
66
68
  if(/^(?:<<\s*:|["'{\[])/.test(line))throw failure('UNSUPPORTED_ROLE_FORMAT');
67
- const field=line.match(/^(name|model|effort|subagent)[ \t]*:\s*(.*)$/);
69
+ const field=line.match(id==='grok'?/^(name|model|effort|subagent|capabilityMode)[ \t]*:\s*(.*)$/:/^(name|model|effort|subagent)[ \t]*:\s*(.*)$/);
68
70
  if(!field)continue;
69
71
  if(Object.hasOwn(fields,field[1]))throw failure('UNSUPPORTED_ROLE_FORMAT');
70
72
  if(field[1]==='subagent'){
71
73
  const boolean=field[2].trim().match(/^(true|false)(?:\s+#.*)?$/);
72
74
  if(!boolean)throw failure('UNSUPPORTED_ROLE_FORMAT');
73
75
  fields.subagent=boolean[1]==='true';
76
+ }else if(field[1]==='capabilityMode'){
77
+ // Keep a value Grok Build would reject without echoing it; it only selects a warning.
78
+ const raw=field[2].trim().replace(/\s+#.*$/,''),quoted=raw.match(/^(["'])(.*)\1$/),value=quoted?quoted[2]:raw;
79
+ fields.capabilityMode=GROK_CAPABILITY_MODES.includes(value)?value:null;
74
80
  }else fields[field[1]]=scalar(field[2]);
75
81
  }
76
82
  if(!fields.name)throw failure('UNSUPPORTED_ROLE_FORMAT');
@@ -139,7 +145,7 @@ async function rolesIn(directory,client,id,budget,models,depth=0){
139
145
  }
140
146
  if(!entry.name.endsWith(id==='chatgpt'?'.toml':'.md'))continue;
141
147
  const body=await readFile(file,budget);if(body===null)throw failure('CONFIG_CHANGED_DURING_READ');
142
- const fields=id==='chatgpt'?parseToml(body):frontmatter(body);
148
+ const fields=id==='chatgpt'?parseToml(body):frontmatter(body,id);
143
149
  const role={name:safeString(fields.name,namePattern),model:optionalString(fields.model),effort:optionalString(id==='chatgpt'?fields.model_reasoning_effort:fields.effort)};
144
150
  if(id==='kimi'){
145
151
  // APK's Markdown role files contain explicit routing guidance, not a
@@ -151,9 +157,12 @@ async function rolesIn(directory,client,id,budget,models,depth=0){
151
157
  }
152
158
  }
153
159
  if(id==='grok'){
154
- // The native per-subagent model override lives in config.toml, not in the agent file.
160
+ // The native per-subagent model override lives in config.toml, not in the agent file; the effort lives in the agent frontmatter.
155
161
  role.model=models.get(role.name)??role.model;
156
- if(fields.effort!==undefined){role.effort=null;warning(client,failure('UNSUPPORTED_ROLE_EFFORT'));}
162
+ // Grok Build drops an agent whose frontmatter effort is not a level it accepts.
163
+ if(role.effort!==null&&!NATIVE_EFFORTS.grok.includes(role.effort))warning(client,failure('UNSUPPORTED_ROLE_EFFORT'));
164
+ // It also drops an agent whose capabilityMode is not a mode it accepts. The snake_case key is not a Grok field.
165
+ if(fields.capabilityMode===null)warning(client,failure('UNSUPPORTED_ROLE_CAPABILITY'));
157
166
  }
158
167
  if(id==='gemini'){
159
168
  if(role.model!==null&&!['pro','flash','flash_lite','inherit'].includes(role.model))throw failure('UNSUPPORTED_NATIVE_SETTING');
package/src/native.mjs CHANGED
@@ -24,9 +24,13 @@ export function nativeRoles(client,config,selectedRoles=ROLE_IDS) {
24
24
  const {description,prompt}=ROLE_DEFINITIONS.find(value=>value.id===role);
25
25
  if(client==='chatgpt') return Object.entries({name,description,model:selected.model,...(selected.effort===null?{}:{model_reasoning_effort:selected.effort}),developer_instructions:prompt}).map(([k,v])=>`${k} = ${JSON.stringify(v)}\n`).join('');
26
26
  const fields=[`name: ${name}`,`description: ${JSON.stringify(description)}`];
27
- // Grok selects the model from config.toml [subagents.models]; read-only roles use the bundled plan permission mode.
27
+ // Grok selects the model from config.toml [subagents.models] and reads a role effort from the agent frontmatter;
28
+ // a null effort writes nothing, so the role keeps its model's native default. Grok Build reads camelCase agent fields:
29
+ // capabilityMode read-only removes the edit, write, delete and shell tools when a subagent of this type starts.
30
+ // Releases 0.4.0 and 0.5.0 wrote `permission_mode: plan`, which Grok Build ignores; `permissionMode: plan` has no effect at spawn.
28
31
  if(client==='grok') {
29
- if(['reviewer','explorer'].includes(role))fields.push('permission_mode: plan');
32
+ if(selected.effort!==null)fields.push(`effort: ${selected.effort}`);
33
+ if(['reviewer','explorer'].includes(role))fields.push('capabilityMode: read-only');
30
34
  } else if(client==='kimi') {
31
35
  if(['reviewer','explorer'].includes(role))fields.push('tools: [Read, Grep, Glob]');
32
36
  } else {
@@ -51,7 +55,7 @@ export function instructionBlock(client,config,maxConcurrent,selectedRoles=ROLE_
51
55
  for(const role of selectedRoles)text.push(`When delegating to apk-${role}, use model tier ${rolePreference(config,role).model}.`);
52
56
  text.push('Per-role effort and a hard concurrency cap are not supported by the documented native agent settings.');
53
57
  }
54
- else if(client==='grok')text.push('Use the named role definitions for model selection. Per-role effort is not supported by the documented native agent settings; subagents use the main session effort. Explicit spawn settings and higher-priority instructions can supersede user defaults.');
58
+ else if(client==='grok')text.push("Use the named role definitions for model and effort selection. A role effort applies only when its model supports effort; a role without an effort uses its model's native default effort. Explicit spawn settings and higher-priority instructions can supersede user defaults.");
55
59
  else if(client==='claude')text.push('Use the named role definitions for model and effort selection. Session, project, managed settings, environment overrides and some spawn paths can supersede these user defaults; the native cap applies to ordinary Agent-tool spawns with documented exceptions.');
56
60
  else text.push('Use the named role definitions for model and effort selection. Explicit spawn settings and higher-priority instructions can supersede user defaults.');
57
61
  if(client==='chatgpt'||client==='claude')text.push('An omitted role effort uses the native client default; it does not pin a level.');
@@ -60,5 +64,5 @@ export function instructionBlock(client,config,maxConcurrent,selectedRoles=ROLE_
60
64
  return text.join('\n')+'\n';
61
65
  }
62
66
  export function nativeWarnings(client) {
63
- return client==='kimi'?['kimi-concurrency-guidance-only','kimi-model-pool-routing-guidance']:client==='gemini'?['gemini-concurrency-guidance-only','gemini-per-role-effort-unsupported']:client==='grok'?['grok-per-role-effort-unsupported','grok-native-precedence-limits']:client==='claude'?['claude-native-precedence-limits']:['chatgpt-native-precedence-limits'];
67
+ return client==='kimi'?['kimi-concurrency-guidance-only','kimi-model-pool-routing-guidance']:client==='gemini'?['gemini-concurrency-guidance-only','gemini-per-role-effort-unsupported']:client==='grok'?['grok-effort-model-dependent','grok-native-precedence-limits']:client==='claude'?['claude-native-precedence-limits']:['chatgpt-native-precedence-limits'];
64
68
  }
package/src/output.mjs CHANGED
@@ -11,7 +11,9 @@ const NOTES={
11
11
  'kimi-model-pool-routing-guidance':'Kimi: the parent selects the model alias for each role.',
12
12
  'gemini-concurrency-guidance-only':'Gemini: the concurrency limit is guidance.',
13
13
  'gemini-per-role-effort-unsupported':'Gemini: native tiers do not support per-role effort.',
14
- 'grok-per-role-effort-unsupported':'Grok: native agent settings do not support per-role effort.',
14
+ 'grok-effort-model-dependent':'Grok: a role effort applies only when its model supports effort; a role without one uses its model default.',
15
+ // Receipts written by 0.4.0 still carry this code; it is shown with the corrected text.
16
+ 'grok-per-role-effort-unsupported':'Grok: a role effort applies only when its model supports effort; a role without one uses its model default.',
15
17
  'grok-native-precedence-limits':'Project and explicit client settings can override these defaults.',
16
18
  'kimi-catalog-alias-resolved':'Kimi: an existing matching model alias was selected.',
17
19
  };
package/src/share.mjs CHANGED
@@ -55,7 +55,7 @@ function shareRelease(release){
55
55
  }
56
56
  function shareChoice(choice,id,metadata){
57
57
  shareRecord(choice,['model','effort']);
58
- if(typeof choice.model!=='string'||choice.model.length>128||(choice.effort!==null&&(['gemini','grok'].includes(id)||!metadata.catalog[id].efforts.includes(choice.effort))))invalidShare();
58
+ if(typeof choice.model!=='string'||choice.model.length>128||(choice.effort!==null&&(id==='gemini'||!metadata.catalog[id].efforts.includes(choice.effort))))invalidShare();
59
59
  return {model:choice.model,effort:choice.effort};
60
60
  }
61
61
  function sharePayload(payload,release,metadata){