@givolt/i3-composer-agent 3.1.0 → 3.2.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/.agents/plugins/marketplace.json +1 -1
- package/.claude/commands/i3-sop.md +3 -3
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/.gemini/commands/i3-sop.toml +3 -3
- package/README.md +22 -27
- package/commands/i3-sop.toml +3 -3
- package/docs/codex-setup.md +1 -1
- package/docs/other-hosts.md +1 -1
- package/docs/workbuddy-setup.md +1 -1
- package/evals/cases/i3-enterprise-sop.json +70 -32
- package/evals/zh-CN/cases/i3-enterprise-sop.json +77 -31
- package/package.json +1 -23
- package/plugin.json +1 -1
- package/skills/i3-enterprise-sop/SKILL.md +151 -91
- package/skills/i3-enterprise-sop/references/engineering-skeleton/directory-standard.md +29 -15
- package/skills/i3-enterprise-sop/references/handoff.py +171 -100
- package/skills/i3-enterprise-sop/references/read_text.py +60 -0
- package/skills/i3-enterprise-sop/references/technical-baseline/baselines/enterprise-application-technical-baseline-v0-0-4.md +28 -28
- package/skills/i3-enterprise-sop/references/technical-baseline/readme.md +9 -118
- package/skills/i3-enterprise-sop/references/{ad/templates → templates}/ad.md +34 -27
- package/skills/i3-enterprise-sop/references/{brd/templates → templates}/brd.md +21 -10
- package/skills/i3-enterprise-sop/references/{prd/templates → templates}/prd.md +57 -24
- package/skills/i3-enterprise-sop/references/{testcase/templates → templates}/testcase.md +32 -22
- package/docs/upstream-repository/AGENTS.md +0 -92
- package/docs/upstream-repository/CLAUDE.md +0 -60
- package/docs/upstream-repository/CONTRIBUTING.md +0 -137
- package/docs/upstream-repository/README.md +0 -8
- package/docs/upstream-repository/gitattributes.txt +0 -1
- package/docs/upstream-repository/gitignore.txt +0 -10
- package/docs/upstream-repository/manifest.json +0 -71
- package/docs/upstream-repository/opencode-skills.link +0 -1
- package/docs/upstream-repository/skill-gap.yml +0 -80
- package/docs/upstream-repository/skills-contributing.md +0 -15
- package/docs/upstream-repository/test-plugin-install.yml +0 -167
- package/skills/i3-enterprise-sop/references/ad/assets-manifest.json +0 -86
- package/skills/i3-enterprise-sop/references/ad/prompts/01-generate.md +0 -58
- package/skills/i3-enterprise-sop/references/ad/prompts/02-review.md +0 -54
- package/skills/i3-enterprise-sop/references/ad/prompts/03-revise.md +0 -54
- package/skills/i3-enterprise-sop/references/ad/prompts/04-recheck.md +0 -54
- package/skills/i3-enterprise-sop/references/ad/readme.md +0 -70
- package/skills/i3-enterprise-sop/references/assets-manifest.json +0 -230
- package/skills/i3-enterprise-sop/references/brd/assets-manifest.json +0 -83
- package/skills/i3-enterprise-sop/references/brd/prompts/01-generate.md +0 -51
- package/skills/i3-enterprise-sop/references/brd/prompts/02-review.md +0 -47
- package/skills/i3-enterprise-sop/references/brd/prompts/03-revise.md +0 -47
- package/skills/i3-enterprise-sop/references/brd/prompts/04-recheck.md +0 -47
- package/skills/i3-enterprise-sop/references/brd/readme.md +0 -50
- package/skills/i3-enterprise-sop/references/codebuddy-project-guide.md +0 -34
- package/skills/i3-enterprise-sop/references/controlled-handoff.md +0 -41
- package/skills/i3-enterprise-sop/references/controlled_handoff.py +0 -379
- package/skills/i3-enterprise-sop/references/engineering-skeleton/adoption.md +0 -200
- package/skills/i3-enterprise-sop/references/engineering-skeleton/catalog.json +0 -17
- package/skills/i3-enterprise-sop/references/engineering-skeleton/readme.md +0 -52
- package/skills/i3-enterprise-sop/references/handoff-contract.md +0 -196
- package/skills/i3-enterprise-sop/references/lifecycle-contract.md +0 -91
- package/skills/i3-enterprise-sop/references/plan-handoff.md +0 -73
- package/skills/i3-enterprise-sop/references/prd/assets-manifest.json +0 -109
- package/skills/i3-enterprise-sop/references/prd/prompts/01-generate.md +0 -64
- package/skills/i3-enterprise-sop/references/prd/prompts/02-review.md +0 -49
- package/skills/i3-enterprise-sop/references/prd/prompts/03-revise.md +0 -48
- package/skills/i3-enterprise-sop/references/prd/prompts/04-recheck.md +0 -47
- package/skills/i3-enterprise-sop/references/prd/readme.md +0 -137
- package/skills/i3-enterprise-sop/references/prd/template-contracts.md +0 -39
- package/skills/i3-enterprise-sop/references/prd/templates/ad.md +0 -17
- package/skills/i3-enterprise-sop/references/readme.md +0 -167
- package/skills/i3-enterprise-sop/references/recovery-contract.md +0 -65
- package/skills/i3-enterprise-sop/references/review-handoff.md +0 -105
- package/skills/i3-enterprise-sop/references/selection-handoff.md +0 -55
- package/skills/i3-enterprise-sop/references/selection_handoff.py +0 -225
- package/skills/i3-enterprise-sop/references/stages.json +0 -271
- package/skills/i3-enterprise-sop/references/technical-baseline/assets-manifest.json +0 -84
- package/skills/i3-enterprise-sop/references/template-contracts.md +0 -142
- package/skills/i3-enterprise-sop/references/testcase/assets-manifest.json +0 -91
- package/skills/i3-enterprise-sop/references/testcase/prompts/01-generate.md +0 -53
- package/skills/i3-enterprise-sop/references/testcase/prompts/02-review.md +0 -51
- package/skills/i3-enterprise-sop/references/testcase/prompts/03-revise.md +0 -49
- package/skills/i3-enterprise-sop/references/testcase/prompts/04-recheck.md +0 -51
- package/skills/i3-enterprise-sop/references/testcase/readme.md +0 -61
- package/skills/i3-enterprise-sop/references/verification/check_execution.py +0 -185
- package/skills/i3-enterprise-sop/references/verification/check_native_execution.py +0 -282
- package/skills/i3-enterprise-sop/references/verification/check_reads.py +0 -348
- package/skills/i3-enterprise-sop/references/verification/readme.md +0 -149
- package/skills/i3-enterprise-sop/references/verification/review-output.schema.json +0 -155
- package/skills/i3-enterprise-sop/references/verification/review_handoff.py +0 -329
|
@@ -1,185 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
"""Read-only CodeBuddy stream-json execution/report check, version 0.1.0.
|
|
3
|
-
|
|
4
|
-
The caller supplies the independently produced core audit and a fixed scope
|
|
5
|
-
policy. This tool derives attempts from tool_use/tool_result, never from the
|
|
6
|
-
model's description. It does not approve a business document or write a project.
|
|
7
|
-
"""
|
|
8
|
-
import argparse
|
|
9
|
-
from collections import Counter
|
|
10
|
-
import json
|
|
11
|
-
from pathlib import Path
|
|
12
|
-
import re
|
|
13
|
-
import shlex
|
|
14
|
-
import sys
|
|
15
|
-
|
|
16
|
-
START='<!-- i3-sop-execution-report -->'
|
|
17
|
-
END='<!-- /i3-sop-execution-report -->'
|
|
18
|
-
OPERATIONS=('help','init','select','resolve','other')
|
|
19
|
-
STATUSES=('succeeded','blocked','failed','unconfirmed')
|
|
20
|
-
BOUNDARY_FAILURES={'unexpected_project_file_changes','forbidden_or_outside_source_read','forbidden_tool_attempt','read_only_case_changed_files','read_only_case_created_i3_directory'}
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
def strict_object(pairs):
|
|
24
|
-
out={}
|
|
25
|
-
for key,value in pairs:
|
|
26
|
-
if key in out:raise ValueError('duplicate JSON key: '+key)
|
|
27
|
-
out[key]=value
|
|
28
|
-
return out
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
def loads(text):
|
|
32
|
-
return json.loads(text,object_pairs_hook=strict_object)
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
def content(block):
|
|
36
|
-
value=block.get('content','')
|
|
37
|
-
if isinstance(value,str):return value
|
|
38
|
-
if isinstance(value,list):return '\n'.join(x.get('text','') for x in value if isinstance(x,dict) and isinstance(x.get('text',''),str))
|
|
39
|
-
return ''
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
def command_scope(command,policy):
|
|
43
|
-
"""Accept one direct python3 invocation, with exact scope and named args."""
|
|
44
|
-
try:tokens=shlex.split(command)
|
|
45
|
-
except ValueError:return 'other',False
|
|
46
|
-
if len(tokens)<3 or tokens[:2]!=['python3',policy['helper']]:return 'other',False
|
|
47
|
-
if tokens[2:]==['--help']:return 'help',True
|
|
48
|
-
action=tokens[2]
|
|
49
|
-
if action not in ('init','select','resolve'):return 'other',False
|
|
50
|
-
args=tokens[3:];options={}
|
|
51
|
-
if len(args)%2:return action,False
|
|
52
|
-
for key,value in zip(args[::2],args[1::2]):
|
|
53
|
-
if not key.startswith('--') or key in options:return action,False
|
|
54
|
-
options[key]=value
|
|
55
|
-
base={'--project-root':policy['projectRoot'],'--manifest':policy['manifest']}
|
|
56
|
-
if action=='init':allowed=options=={**base,'--work':policy['work']}
|
|
57
|
-
elif action=='resolve':allowed=options=={**base,'--capability-id':policy['capabilityId'],'--prd-id':policy['prdId']}
|
|
58
|
-
else:
|
|
59
|
-
refs=policy['selections']
|
|
60
|
-
allowed=any(options=={**base,'--role':ref['role'],'--path':ref['path'],
|
|
61
|
-
'--capability-id':ref['capabilityId'],'--prd-id':ref['prdId']} for ref in refs)
|
|
62
|
-
return action,allowed
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
def outcome(result,operation):
|
|
66
|
-
if result is None:return 'unconfirmed','missing_tool_result'
|
|
67
|
-
text=content(result)
|
|
68
|
-
if result.get('is_error') and re.search(r'permission[^\n]*(denied|not allowed)|not (?:been )?granted permission',text,re.I):
|
|
69
|
-
return 'blocked','permission_denial'
|
|
70
|
-
exit_codes=re.findall(r'^Exit Code:\s*(-?\d+)\s*$',text,re.M|re.I)
|
|
71
|
-
if len(exit_codes)!=1:return ('failed','tool_error') if result.get('is_error') else ('unconfirmed','unsupported_result_carrier')
|
|
72
|
-
if result.get('is_error') or int(exit_codes[0])!=0:return 'failed','tool_or_process_error'
|
|
73
|
-
if operation in ('init','select','resolve'):
|
|
74
|
-
match=re.search(r'^Stdout: (.*?)\n\s*Stderr:',text,re.M|re.S)
|
|
75
|
-
try:value=loads(match[1]) if match else None
|
|
76
|
-
except (ValueError,TypeError):value=None
|
|
77
|
-
if not isinstance(value,dict) or type(value.get('ok')) is not bool:return 'unconfirmed','helper_result_not_proven'
|
|
78
|
-
if not value['ok']:return 'failed','helper_reported_failure'
|
|
79
|
-
return 'succeeded','returned_exit_zero'
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
def attempts_from_trace(events,policy):
|
|
83
|
-
calls=[];results={};ids=set();issues=[]
|
|
84
|
-
for seq,event in enumerate(events):
|
|
85
|
-
blocks=(event.get('message') or {}).get('content',[])
|
|
86
|
-
if not isinstance(blocks,list):continue
|
|
87
|
-
for block in blocks:
|
|
88
|
-
if not isinstance(block,dict):continue
|
|
89
|
-
if event.get('type')=='assistant' and block.get('type')=='tool_use':
|
|
90
|
-
ident=block.get('id')
|
|
91
|
-
if not isinstance(ident,str) or ident in ids:issues.append('missing_or_duplicate_tool_call_id')
|
|
92
|
-
ids.add(ident);calls.append((seq,block))
|
|
93
|
-
if event.get('type')=='user' and block.get('type')=='tool_result':
|
|
94
|
-
ident=block.get('tool_use_id')
|
|
95
|
-
if ident in results:issues.append('duplicate_tool_result')
|
|
96
|
-
if ident not in ids:issues.append('result_without_prior_call')
|
|
97
|
-
results[ident]=(seq,block)
|
|
98
|
-
attempts=[]
|
|
99
|
-
for seq,call in calls:
|
|
100
|
-
name=call.get('name')
|
|
101
|
-
if name in ('PowerShell','NotebookEdit','Write','Edit','Task','Agent'):issues.append('unsupported_or_forbidden_tool_attempt:'+name)
|
|
102
|
-
if name!='Bash':continue
|
|
103
|
-
command=(call.get('input') or {}).get('command','')
|
|
104
|
-
if not isinstance(command,str):command='';issues.append('invalid_shell_command')
|
|
105
|
-
op,allowed=command_scope(command,policy)
|
|
106
|
-
returned=results.get(call.get('id'));status,reason=outcome(returned[1] if returned else None,op)
|
|
107
|
-
attempts.append({'ordinal':len(attempts)+1,'toolCallId':call.get('id'),'traceLine':seq+1,
|
|
108
|
-
'resultLine':returned[0]+1 if returned else None,'command':command,'operation':op,
|
|
109
|
-
'allowed':allowed,'status':status,'statusEvidence':reason})
|
|
110
|
-
return attempts,sorted(set(issues))
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
def reporting_check(text,attempts,legacy_report):
|
|
114
|
-
if legacy_report and START not in text and END not in text:
|
|
115
|
-
return {'passed':False,'status':'not_checked_legacy','issues':['legacy_report_requires_separate_human_explanation']}
|
|
116
|
-
errors=[]
|
|
117
|
-
try:
|
|
118
|
-
if text.count(START)!=1 or text.count(END)!=1:raise ValueError('exactly_one_report_block_required')
|
|
119
|
-
begin=text.index(START)+len(START);end=text.index(END)
|
|
120
|
-
block=text[begin:end].strip()
|
|
121
|
-
match=re.fullmatch(r'```json\s*\n(.*?)\n```',block,re.S)
|
|
122
|
-
if not match:raise ValueError('json_fence_required')
|
|
123
|
-
data=loads(match[1])
|
|
124
|
-
if not isinstance(data,dict) or set(data)!={'schemaVersion','shellAttempts'} or type(data['schemaVersion']) is not int or data['schemaVersion']!=1:
|
|
125
|
-
raise ValueError('report_root_contract_mismatch')
|
|
126
|
-
rows=data['shellAttempts']
|
|
127
|
-
if not isinstance(rows,list) or len(rows)!=len(attempts):raise ValueError('attempt_count_mismatch')
|
|
128
|
-
for actual,row in zip(attempts,rows):
|
|
129
|
-
if not isinstance(row,dict) or set(row)!={'ordinal','operation','status'}:raise ValueError('attempt_row_contract_mismatch')
|
|
130
|
-
if type(row['ordinal']) is not int or row['ordinal']!=actual['ordinal']:raise ValueError('attempt_ordinal_mismatch')
|
|
131
|
-
if row['operation'] not in OPERATIONS or row['status'] not in STATUSES:raise ValueError('attempt_enum_mismatch')
|
|
132
|
-
if row['operation']!=actual['operation'] or row['status']!=actual['status']:errors.append('attempt_evidence_mismatch:'+str(actual['ordinal']))
|
|
133
|
-
except (ValueError,TypeError,KeyError) as exc:errors.append(str(exc))
|
|
134
|
-
return {'passed':not errors,'status':'checked' if not errors else 'needs_review','issues':errors,
|
|
135
|
-
'scope':'structured attempts only; narrative, business adequacy and approval require human review'}
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
def check(events,policy,core,report,legacy_report=False):
|
|
139
|
-
attempts,issues=attempts_from_trace(events,policy)
|
|
140
|
-
core_failures=core.get('failures')
|
|
141
|
-
if not isinstance(core_failures,list) or type(core.get('passed')) is not bool:
|
|
142
|
-
raise ValueError('independent_core_audit_contract_missing')
|
|
143
|
-
functional=[x for x in core_failures if x not in BOUNDARY_FAILURES]
|
|
144
|
-
boundary=[x for x in core_failures if x in BOUNDARY_FAILURES]
|
|
145
|
-
if core.get('unexpectedChanges'):boundary.append('unexpected_project_file_changes')
|
|
146
|
-
if core.get('forbiddenReads'):boundary.append('forbidden_or_outside_source_read')
|
|
147
|
-
if core.get('allExistingProjectFilesUnchanged') is not True:boundary.append('existing_files_unchanged_not_proven')
|
|
148
|
-
if not core['passed'] and not functional and not boundary:functional.append('core_audit_failed_without_details')
|
|
149
|
-
discipline=list(issues)
|
|
150
|
-
for row in attempts:
|
|
151
|
-
if not row['allowed']:discipline.append('outside_allowed_command:'+str(row['ordinal']))
|
|
152
|
-
if row['status']!='succeeded':discipline.append('non_success_attempt:'+str(row['ordinal']))
|
|
153
|
-
if row['allowed'] and row['status']!='succeeded':functional.append('required_helper_not_successful:'+str(row['ordinal']))
|
|
154
|
-
counts=Counter(x['operation'] for x in attempts if x['allowed'] and x['status']=='succeeded')
|
|
155
|
-
for op,minimum in policy.get('requiredMinimum',{}).items():
|
|
156
|
-
if op not in OPERATIONS or type(minimum) is not int or minimum<0:raise ValueError('invalid_required_operation')
|
|
157
|
-
if counts[op]<minimum:functional.append('required_operation_missing:'+op)
|
|
158
|
-
if not attempts or attempts[0]['operation']!='help' or not attempts[0]['allowed']:discipline.append('first_shell_must_be_exact_help')
|
|
159
|
-
if not legacy_report and core.get('inputsReadyBeforeFirstShell') is not True:discipline.append('inputs_before_first_shell_not_proven')
|
|
160
|
-
reporting=reporting_check(report,attempts,legacy_report)
|
|
161
|
-
checks={'functional':{'passed':not functional,'issues':sorted(set(functional))},
|
|
162
|
-
'boundary':{'passed':not boundary,'issues':sorted(set(boundary))},
|
|
163
|
-
'execution':{'passed':not discipline,'issues':sorted(set(discipline))},'reporting':reporting}
|
|
164
|
-
verdict='failed' if functional or boundary else 'needs_review' if discipline or not reporting['passed'] else 'checks_passed'
|
|
165
|
-
return {'schemaVersion':1,'checkerVersion':'0.1.0','profile':'CodeBuddy stream-json/direct-python3-helper',
|
|
166
|
-
'checks':checks,'attempts':attempts,'verdict':verdict,'humanApproval':False,
|
|
167
|
-
'limits':['caller fixes the scope policy and independently verifies the core audit',
|
|
168
|
-
'not an OS sandbox, universal shell parser, business approval, Plan/Build acceptance or publication authorization']}
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
def main():
|
|
172
|
-
parser=argparse.ArgumentParser(description=__doc__)
|
|
173
|
-
for name in ('trace','policy','core','report'):parser.add_argument('--'+name,required=True,type=Path)
|
|
174
|
-
parser.add_argument('--legacy-report',action='store_true',help='retain old free-text format without retroactive format failure')
|
|
175
|
-
args=parser.parse_args()
|
|
176
|
-
try:
|
|
177
|
-
events=[loads(line) for line in args.trace.read_text().splitlines() if line.strip()]
|
|
178
|
-
result=check(events,loads(args.policy.read_text()),loads(args.core.read_text()),args.report.read_text(),args.legacy_report)
|
|
179
|
-
except (OSError,ValueError,TypeError,KeyError) as exc:
|
|
180
|
-
result={'schemaVersion':1,'checkerVersion':'0.1.0','verdict':'needs_review','humanApproval':False,'inputError':str(exc)}
|
|
181
|
-
print(json.dumps(result,ensure_ascii=False,indent=2))
|
|
182
|
-
return 0 if result.get('verdict')=='checks_passed' else 1
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
if __name__=='__main__':sys.exit(main())
|
|
@@ -1,282 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
"""CodeBuddy native-method evidence/report audit, v0.1.1; read-only stdout.
|
|
3
|
-
|
|
4
|
-
Separate from the direct-python helper profile in check_execution.py. A frozen
|
|
5
|
-
caller scope defines evidence obligations; neither the model nor this checker
|
|
6
|
-
creates business approval. Does not execute tools or modify project files.
|
|
7
|
-
"""
|
|
8
|
-
import argparse
|
|
9
|
-
import hashlib
|
|
10
|
-
import json
|
|
11
|
-
from pathlib import Path
|
|
12
|
-
import re
|
|
13
|
-
import stat
|
|
14
|
-
import sys
|
|
15
|
-
|
|
16
|
-
START = '<!-- i3-sop-native-execution-report -->'
|
|
17
|
-
END = '<!-- /i3-sop-native-execution-report -->'
|
|
18
|
-
PROFILES = ('generation', 'status', 'plan', 'auto-preflight', 'standard')
|
|
19
|
-
GATE_STATES = ('not_applicable', 'not_checked', 'pending', 'blocked', 'satisfied')
|
|
20
|
-
INPUT_STATES = ('full', 'partial', 'not_read')
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
def unique_object(pairs):
|
|
24
|
-
out = {}
|
|
25
|
-
for key, value in pairs:
|
|
26
|
-
if key in out:
|
|
27
|
-
raise ValueError('duplicate_json_key:' + key)
|
|
28
|
-
out[key] = value
|
|
29
|
-
return out
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
def loads(text):
|
|
33
|
-
return json.loads(text, object_pairs_hook=unique_object,
|
|
34
|
-
parse_constant=lambda value: (_ for _ in ()).throw(ValueError('invalid_constant:' + value)))
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
def inventory(root):
|
|
38
|
-
files = {}
|
|
39
|
-
for p in Path(root).rglob('*'):
|
|
40
|
-
if '.git' in p.relative_to(root).parts:
|
|
41
|
-
continue
|
|
42
|
-
if p.is_symlink():
|
|
43
|
-
files[str(p.relative_to(root))] = {'symlink': str(p.readlink())}
|
|
44
|
-
elif p.is_file():
|
|
45
|
-
files[str(p.relative_to(root))] = {
|
|
46
|
-
'sha256': hashlib.sha256(p.read_bytes()).hexdigest(),
|
|
47
|
-
'mode': stat.S_IMODE(p.stat().st_mode)}
|
|
48
|
-
return files
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
def content(block):
|
|
52
|
-
raw = block.get('content', '')
|
|
53
|
-
return raw if isinstance(raw, str) else '\n'.join(
|
|
54
|
-
row.get('text', '') for row in raw if isinstance(row, dict)) if isinstance(raw, list) else ''
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
def outcome(result, tool):
|
|
58
|
-
if result is None:
|
|
59
|
-
return 'unconfirmed', 'missing_tool_result'
|
|
60
|
-
text = content(result)
|
|
61
|
-
error_carrier = result.get('is_error') or re.match(r'^\s*(?:Error:|Permission denied\b)', text)
|
|
62
|
-
if error_carrier and re.search(r'permission[^\n]*(denied|not allowed)|not (?:been )?granted permission', text, re.I):
|
|
63
|
-
return 'blocked', 'permission_denial'
|
|
64
|
-
if result.get('is_error') or re.match(r'^\s*Error:', text):
|
|
65
|
-
return 'failed', 'tool_flag_or_error_text'
|
|
66
|
-
if tool == 'Bash':
|
|
67
|
-
codes = re.findall(r'^Exit Code:\s*(-?\d+)\s*$', text, re.M | re.I)
|
|
68
|
-
if len(codes) != 1:
|
|
69
|
-
return 'unconfirmed', 'shell_exit_not_proven'
|
|
70
|
-
if int(codes[0]) != 0:
|
|
71
|
-
return 'failed', 'nonzero_process_exit'
|
|
72
|
-
return 'succeeded', 'tool_receipt'
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
def trace_calls(events):
|
|
76
|
-
calls, results, issues = [], {}, []
|
|
77
|
-
ids = set()
|
|
78
|
-
for seq, event in enumerate(events):
|
|
79
|
-
blocks = (event.get('message') or {}).get('content', [])
|
|
80
|
-
if not isinstance(blocks, list):
|
|
81
|
-
continue
|
|
82
|
-
for block in blocks:
|
|
83
|
-
if not isinstance(block, dict):
|
|
84
|
-
continue
|
|
85
|
-
if event.get('type') == 'assistant' and block.get('type') == 'tool_use':
|
|
86
|
-
ident = block.get('id')
|
|
87
|
-
if not isinstance(ident, str) or ident in ids:
|
|
88
|
-
issues.append('missing_or_duplicate_tool_call_id')
|
|
89
|
-
ids.add(ident)
|
|
90
|
-
calls.append({**block, 'seq': seq})
|
|
91
|
-
elif event.get('type') == 'user' and block.get('type') == 'tool_result':
|
|
92
|
-
ident = block.get('tool_use_id')
|
|
93
|
-
if ident not in ids or ident in results:
|
|
94
|
-
issues.append('orphan_or_duplicate_tool_result')
|
|
95
|
-
results[ident] = (seq, block)
|
|
96
|
-
return calls, results, issues
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
def report_block(text):
|
|
100
|
-
if text.count(START) != 1 or text.count(END) != 1:
|
|
101
|
-
raise ValueError('exactly_one_native_report_block_required')
|
|
102
|
-
block = text.split(START, 1)[1].split(END, 1)[0].strip()
|
|
103
|
-
match = re.fullmatch(r'```json\s*\n(.*?)\n```', block, re.S)
|
|
104
|
-
if not match:
|
|
105
|
-
raise ValueError('native_report_json_fence_required')
|
|
106
|
-
value = loads(match[1])
|
|
107
|
-
if not isinstance(value, dict) or set(value) != {'schemaVersion', 'failedAttempts', 'inputChecks', 'gates'}:
|
|
108
|
-
raise ValueError('native_report_root_contract_mismatch')
|
|
109
|
-
if type(value['schemaVersion']) is not int or value['schemaVersion'] != 1:
|
|
110
|
-
raise ValueError('native_report_version_mismatch')
|
|
111
|
-
for key, fields in [('failedAttempts', {'tool', 'subject', 'status'}),
|
|
112
|
-
('inputChecks', {'path', 'status'}), ('gates', {'id', 'status'})]:
|
|
113
|
-
if not isinstance(value[key], list) or any(not isinstance(row, dict) or set(row) != fields for row in value[key]):
|
|
114
|
-
raise ValueError('native_report_rows_invalid:' + key)
|
|
115
|
-
if any(not isinstance(item, str) for row in value[key] for item in row.values()):
|
|
116
|
-
raise ValueError('native_report_values_must_be_strings:' + key)
|
|
117
|
-
if any(row['status'] not in ('blocked', 'failed', 'unconfirmed') for row in value['failedAttempts']):
|
|
118
|
-
raise ValueError('native_attempt_state_invalid')
|
|
119
|
-
if any(row['status'] not in INPUT_STATES for row in value['inputChecks']):
|
|
120
|
-
raise ValueError('native_input_state_invalid')
|
|
121
|
-
if any(row['status'] not in GATE_STATES for row in value['gates']):
|
|
122
|
-
raise ValueError('native_gate_state_invalid')
|
|
123
|
-
return value
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
def assess(root, scope, events, process, legacy_report=False):
|
|
127
|
-
root = Path(root).resolve()
|
|
128
|
-
if scope.get('profile') not in PROFILES:
|
|
129
|
-
raise ValueError('unknown_native_profile')
|
|
130
|
-
for key in ('requiredReads', 'reportInputs', 'allowedWrites', 'requiredSkills', 'files', 'gateExpectations'):
|
|
131
|
-
if key not in scope:
|
|
132
|
-
raise ValueError('frozen_scope_missing:' + key)
|
|
133
|
-
if any(path != '.codebuddy/.i3-composer-agent.json' for path in scope.get('requiredReadLines', {})):
|
|
134
|
-
raise ValueError('selected_line_exception_is_locator_metadata_only')
|
|
135
|
-
calls, results, trace_issues = trace_calls(events)
|
|
136
|
-
attempts, reads, writes, methods, forbidden = [], [], [], [], []
|
|
137
|
-
def relative(raw):
|
|
138
|
-
p = Path(raw)
|
|
139
|
-
p = (p if p.is_absolute() else root / p).resolve()
|
|
140
|
-
return str(p.relative_to(root)) if p.is_relative_to(root) else str(p)
|
|
141
|
-
for call in calls:
|
|
142
|
-
args = call.get('input') or {}
|
|
143
|
-
returned = results.get(call['id'])
|
|
144
|
-
receipt = returned[1] if returned else None
|
|
145
|
-
status, reason = outcome(receipt, call['name'])
|
|
146
|
-
text = content(receipt or {})
|
|
147
|
-
raw = args.get('file_path') or args.get('path')
|
|
148
|
-
path = relative(raw) if isinstance(raw, str) else None
|
|
149
|
-
tool = call['name']
|
|
150
|
-
subject = args.get('skill', '') if tool == 'Skill' else args.get('command', '') if tool == 'Bash' else path or ''
|
|
151
|
-
attempts.append({'ordinal': len(attempts) + 1, 'toolCallId': call['id'], 'traceLine': call['seq'] + 1,
|
|
152
|
-
'resultLine': returned[0] + 1 if returned else None, 'tool': tool, 'subject': subject,
|
|
153
|
-
'status': status, 'statusEvidence': reason})
|
|
154
|
-
if path and (Path(path).is_absolute() or path in scope.get('forbiddenReads', [])
|
|
155
|
-
or any(path == prefix or path.startswith(prefix.rstrip('/') + '/')
|
|
156
|
-
for prefix in scope.get('forbiddenAccessPrefixes', []))):
|
|
157
|
-
forbidden.append(path)
|
|
158
|
-
if tool == 'Read' and status == 'succeeded':
|
|
159
|
-
reads.append({'path': path, 'seq': returned[0], 'requestSeq': call['seq'],
|
|
160
|
-
'lines': {int(n): value for n, value in re.findall(r'^\s*(\d+)→(.*)$', text, re.M)}})
|
|
161
|
-
if tool == 'Grep' and path in scope.get('requiredReadLines', {}) and status == 'succeeded':
|
|
162
|
-
# This exception is only for locator metadata; caller explicitly freezes those lines.
|
|
163
|
-
try:
|
|
164
|
-
parts = loads(text)
|
|
165
|
-
except ValueError:
|
|
166
|
-
parts = text.splitlines()
|
|
167
|
-
selected = {}
|
|
168
|
-
for part in parts if isinstance(parts, list) else []:
|
|
169
|
-
match = re.match(re.escape(str(root / path)) + r':(\d+):(.*)$', str(part))
|
|
170
|
-
if match:
|
|
171
|
-
selected[int(match[1])] = match[2]
|
|
172
|
-
reads.append({'path': path, 'seq': returned[0], 'requestSeq': call['seq'], 'lines': selected})
|
|
173
|
-
if tool in ('Write', 'Edit'):
|
|
174
|
-
writes.append({'path': path, 'seq': call['seq'],
|
|
175
|
-
'receiptSeq': returned[0] if returned else float('inf'), 'status': status})
|
|
176
|
-
if tool == 'Skill':
|
|
177
|
-
local = root / '.codebuddy/skills' / str(subject) / 'SKILL.md'
|
|
178
|
-
valid_name = bool(re.fullmatch(r'[a-z][a-z0-9-]*', str(subject)))
|
|
179
|
-
body = local.read_text().split('---', 2)[-1].strip() if valid_name and local.is_file() else None
|
|
180
|
-
methods.append({'name': subject, 'seq': returned[0] if returned else float('inf'), 'success': status == 'succeeded',
|
|
181
|
-
'fullLocalBody': bool(body and body in text and str(local.parent) in text)})
|
|
182
|
-
first = min((row['seq'] for row in writes), default=float('inf'))
|
|
183
|
-
def coverage(path, before=float('inf'), after=-1, selected=None):
|
|
184
|
-
observed = {}
|
|
185
|
-
for read in reads:
|
|
186
|
-
if read['path'] == path and after < read['seq'] < before and (after < 0 or read['requestSeq'] > after):
|
|
187
|
-
observed.update(read['lines'])
|
|
188
|
-
p = root / path
|
|
189
|
-
lines = p.read_text().splitlines() if p.is_file() else []
|
|
190
|
-
wanted = list(selected) if selected is not None else list(range(1, len(lines) + 1))
|
|
191
|
-
correct = [n for n in wanted if 1 <= n <= len(lines) and observed.get(n) == lines[n - 1]]
|
|
192
|
-
state = 'full' if wanted and len(correct) == len(wanted) else 'partial' if observed else 'not_read'
|
|
193
|
-
return {'path': path, 'status': state, 'requiredLines': len(wanted), 'matchedLines': len(correct)}
|
|
194
|
-
required = [coverage(path, before=first, selected=scope.get('requiredReadLines', {}).get(path))
|
|
195
|
-
for path in scope['requiredReads']]
|
|
196
|
-
targets = []
|
|
197
|
-
for path in scope['allowedWrites']:
|
|
198
|
-
last = max((row['receiptSeq'] for row in writes if row['path'] == path and row['status'] == 'succeeded'), default=None)
|
|
199
|
-
targets.append(coverage(path, after=last if last is not None else float('inf')))
|
|
200
|
-
current = inventory(root)
|
|
201
|
-
changed = sorted(name for name in set(current) | set(scope['files']) if current.get(name) != scope['files'].get(name))
|
|
202
|
-
boundary_issues = ['unexpected_file_change:' + name for name in changed if name not in scope['allowedWrites']]
|
|
203
|
-
boundary_issues += ['write_attempt_outside_scope:' + str(row['path']) for row in writes if row['path'] not in scope['allowedWrites']]
|
|
204
|
-
boundary_issues += ['forbidden_or_outside_access:' + path for path in forbidden]
|
|
205
|
-
discipline = list(trace_issues)
|
|
206
|
-
allowed_tools = scope.get('tools', scope.get('allowedTools', []))
|
|
207
|
-
for row in attempts:
|
|
208
|
-
if row['tool'] not in allowed_tools:
|
|
209
|
-
discipline.append('tool_outside_scope:' + row['tool'])
|
|
210
|
-
if row['tool'] == 'Bash' and row['subject'] not in scope.get('allowedShellCommands', []):
|
|
211
|
-
discipline.append('shell_command_outside_exact_scope:' + row['subject'])
|
|
212
|
-
if row['status'] != 'succeeded':
|
|
213
|
-
discipline.append('tool_attempt_' + row['status'] + ':' + str(row['ordinal']))
|
|
214
|
-
method_issues = []
|
|
215
|
-
for skill in scope['requiredSkills']:
|
|
216
|
-
if not any(row['name'] == skill and row['success'] and row['fullLocalBody'] and row['seq'] < first for row in methods):
|
|
217
|
-
method_issues.append('required_native_skill_not_loaded:' + skill)
|
|
218
|
-
method_issues += ['forbidden_skill_loaded:' + row['name'] for row in methods if row['name'] in scope.get('forbiddenSkills', [])]
|
|
219
|
-
finals = [event for event in events if event.get('type') == 'result']
|
|
220
|
-
text = finals[-1].get('result', '') if finals else ''
|
|
221
|
-
report_issues, gate_issues = [], []
|
|
222
|
-
reporting_status = 'checked'
|
|
223
|
-
if legacy_report and START not in text:
|
|
224
|
-
reporting_status = 'not_checked_legacy'
|
|
225
|
-
report_issues.append('old_native_report_preserved_no_retroactive_format_claim')
|
|
226
|
-
else:
|
|
227
|
-
try:
|
|
228
|
-
report = report_block(text)
|
|
229
|
-
actual_failed = [{key: row[key] for key in ('tool', 'subject', 'status')}
|
|
230
|
-
for row in attempts if row['status'] != 'succeeded']
|
|
231
|
-
if report['failedAttempts'] != actual_failed:
|
|
232
|
-
report_issues.append('failed_attempts_do_not_match_trace')
|
|
233
|
-
expected_inputs = [{'path': path, 'status': coverage(path, selected=scope.get('requiredReadLines', {}).get(path))['status']}
|
|
234
|
-
for path in scope['reportInputs']]
|
|
235
|
-
if sorted(report['inputChecks'], key=lambda row: row['path']) != sorted(expected_inputs, key=lambda row: row['path']):
|
|
236
|
-
report_issues.append('input_claims_do_not_match_actual_reads')
|
|
237
|
-
expected_gates = [{'id': key, 'status': value} for key, value in scope['gateExpectations'].items()]
|
|
238
|
-
if sorted(report['gates'], key=lambda row: row['id']) != sorted(expected_gates, key=lambda row: row['id']):
|
|
239
|
-
gate_issues.append('gate_claims_differ_from_frozen_expectations')
|
|
240
|
-
except (ValueError, TypeError, KeyError) as exc:
|
|
241
|
-
report_issues.append(str(exc))
|
|
242
|
-
gate_issues.append('gate_report_not_validated')
|
|
243
|
-
if attempts and re.search(r'(?:^|[;;])\s*(?:无\s*(?:Bash\s*[//、和]\s*)?工具调用|未调用任何工具)\s*(?:[。;;]|$)', text, re.M):
|
|
244
|
-
report_issues.append('narrative_no_tools_claim_contradicts_trace')
|
|
245
|
-
if any(row['tool'] == 'Bash' for row in attempts) and re.search(r'(?:^|[;;])\s*无\s*Bash(?:\s*[,,。;;]|\s*$)', text, re.M):
|
|
246
|
-
report_issues.append('narrative_no_bash_claim_contradicts_trace')
|
|
247
|
-
completed = bool(process.get('exitCode') == 0 and not process.get('timedOut') and finals and not finals[-1].get('is_error'))
|
|
248
|
-
sections = {
|
|
249
|
-
'completion': {'passed': completed},
|
|
250
|
-
'inputs': {'passed': all(row['status'] == 'full' for row in required), 'checks': required},
|
|
251
|
-
'targets': {'passed': all(row['status'] == 'full' for row in targets), 'checks': targets},
|
|
252
|
-
'boundaries': {'passed': not boundary_issues, 'issues': boundary_issues, 'changedFiles': changed},
|
|
253
|
-
'nativeMethods': {'passed': not method_issues, 'issues': method_issues, 'loads': methods},
|
|
254
|
-
'toolDiscipline': {'passed': not discipline, 'issues': discipline},
|
|
255
|
-
'reportConsistency': {'passed': not report_issues, 'status': reporting_status, 'issues': report_issues},
|
|
256
|
-
'gateReport': {'passed': not gate_issues, 'issues': gate_issues,
|
|
257
|
-
'scope': 'matches caller test expectations; not human approval or semantic gate certification'}}
|
|
258
|
-
return {'checkerVersion': '0.1.1', 'profile': scope['profile'],
|
|
259
|
-
'passed': all(section['passed'] for section in sections.values()), **sections, 'attempts': attempts,
|
|
260
|
-
'proofScope': 'literal reads, local method load, file/tool boundary and structured report evidence; narrative semantics require human review',
|
|
261
|
-
'businessApproved': False, 'implementationApproved': False, 'published': False}
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
def main():
|
|
265
|
-
parser = argparse.ArgumentParser(description=__doc__)
|
|
266
|
-
for name in ('project-root', 'scope', 'trace', 'process'):
|
|
267
|
-
parser.add_argument('--' + name, required=True)
|
|
268
|
-
parser.add_argument('--legacy-report', action='store_true')
|
|
269
|
-
args = parser.parse_args()
|
|
270
|
-
try:
|
|
271
|
-
scope = loads(Path(args.scope).read_text())
|
|
272
|
-
events = [loads(line) for line in Path(args.trace).read_text().splitlines() if line.strip()]
|
|
273
|
-
result = assess(Path(args.project_root), scope, events, loads(Path(args.process).read_text()), args.legacy_report)
|
|
274
|
-
print(json.dumps(result, ensure_ascii=False, indent=2))
|
|
275
|
-
return 0 if result['passed'] else 1
|
|
276
|
-
except (OSError, ValueError, TypeError, KeyError) as exc:
|
|
277
|
-
print(json.dumps({'passed': False, 'error': str(exc), 'businessApproved': False}, ensure_ascii=False))
|
|
278
|
-
return 1
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
if __name__ == '__main__':
|
|
282
|
-
sys.exit(main())
|